diff --git a/scripts/apply-flag-migration.ts b/scripts/apply-flag-migration.ts deleted file mode 100644 index 74d195d..0000000 --- a/scripts/apply-flag-migration.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * apply-flag-migration.ts - * - * Applies the flagged_content table migration to Turso. - * Run with: npx tsx scripts/apply-flag-migration.ts - */ - -import dotenv from "dotenv"; -import path from "node:path"; - -const envFile = - process.env.NODE_ENV === "production" ? "../.env.production" : "../.env.development"; -dotenv.config({ path: path.resolve(__dirname, envFile) }); - -import { createClient } from "@libsql/client"; - -async function main() { - const db = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - console.log("Applying migration: create flagged_content table..."); - - await db.execute(` - CREATE TABLE IF NOT EXISTS flagged_content ( - id text PRIMARY KEY NOT NULL, - content_type text NOT NULL, - content_id text NOT NULL, - field_name text NOT NULL, - notes text DEFAULT '', - flag_count integer DEFAULT 1 NOT NULL, - created_at text DEFAULT (datetime('now')) NOT NULL, - updated_at text DEFAULT (datetime('now')) NOT NULL - ) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_flagged_content_type ON flagged_content (content_type) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_flagged_content_id ON flagged_content (content_id) - `); - - console.log("Migration applied successfully."); - db.close(); -} - -main().catch((err) => { - console.error("Migration failed:", err); - process.exit(1); -}); diff --git a/scripts/apply-migration.ts b/scripts/apply-migration.ts deleted file mode 100644 index 5564425..0000000 --- a/scripts/apply-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "dotenv/config"; -import { createClient } from "@libsql/client"; - -async function main() { - const db = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - console.log("Applying migration: add image_url to diseases..."); - await db.execute("ALTER TABLE diseases ADD COLUMN image_url TEXT DEFAULT ''"); - await db.execute("UPDATE diseases SET image_url = '' WHERE image_url IS NULL"); - - // Mark migration as applied - await db.execute( - "INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('0001_add-disease-images', datetime('now'))", - ); - - console.log("Migration applied successfully."); - db.close(); -} - -main().catch(console.error); diff --git a/scripts/check-progress.mjs b/scripts/check-progress.mjs deleted file mode 100644 index 31a8ac7..0000000 --- a/scripts/check-progress.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import { createClient } from "@libsql/client"; -const c = createClient({ - url: process.env.DATABASE_URL, - authToken: process.env.DATABASE_TOKEN, -}); -const r = await c.execute("SELECT COUNT(*) as cnt FROM diseases"); -const r2 = await c.execute( - `SELECT SUM(CASE WHEN image_url IS NOT NULL AND image_url != '' THEN 1 ELSE 0 END) as has, SUM(CASE WHEN image_url IS NULL OR image_url = '' THEN 1 ELSE 0 END) as miss FROM diseases`, -); -const r3 = await c.execute( - `SELECT severity, COUNT(*) as total, SUM(CASE WHEN image_url IS NOT NULL AND image_url != '' THEN 1 ELSE 0 END) as has FROM diseases GROUP BY severity ORDER BY severity`, -); -console.log( - `Total: ${r.rows[0].cnt} | With images: ${r2.rows[0].has} | Missing: ${r2.rows[0].miss}`, -); -for (const row of r3.rows) { - console.log(` ${row.severity?.padEnd(10)}: ${row.has}/${row.total}`); -} -c.close(); diff --git a/scripts/disease-templates.ts b/scripts/disease-templates.ts deleted file mode 100644 index f8f048b..0000000 --- a/scripts/disease-templates.ts +++ /dev/null @@ -1,2337 +0,0 @@ -/** - * Disease templates for the plant disease knowledge base. - * All templates are sourced from UW-Madison PDDC and Cornell PDDC factsheets. - * - * Organized by: - * - Generic templates (cross-family) - * - Family-specific templates (e.g., Solanaceae, Cucurbitaceae) - */ - -import type { CausalAgentType, Severity } from "../src/lib/types"; - -// ─── Core template structure ──────────────────────────────────────────────── - -export interface DiseaseSpec { - name: string; - sciName: string; - type: CausalAgentType; - severity: Severity; - symptoms: string[]; - causes: string[]; - treatment: string[]; - prevention: string[]; -} - -// ─── Generic / Cross-family Templates ─────────────────────────────────────── -// These diseases can affect a wide range of plant species - -export const GENERIC_TEMPLATES: DiseaseSpec[] = [ - { - name: "Powdery Mildew", - sciName: "Erysiphe spp., Sphaerotheca spp., Podosphaera spp.", - type: "fungal", - severity: "moderate", - symptoms: [ - "White to grayish powdery fungal growth on upper surfaces of leaves and young stems", - "Yellowing and browning of infected leaves starting from leaf margins", - "Distorted, stunted, or curled new growth and flower buds", - "Premature leaf drop and reduced photosynthesis in severe cases", - "Reduced fruit yield and quality, with small or malformed fruit", - ], - causes: [ - "Fungal spores overwintering on plant debris or in dormant buds", - "High relative humidity (not free water) combined with moderate temperatures (60-80°F)", - "Dense plantings with poor air circulation that trap humidity around foliage", - "Shaded conditions and excess nitrogen fertilization promoting succulent growth", - "Spores easily spread by wind over considerable distances", - ], - treatment: [ - "Apply sulfur-based fungicide, potassium bicarbonate, or neem oil at first sign of infection", - "Remove and destroy heavily infected leaves, stems, and flower buds", - "Improve air circulation through pruning, thinning, and proper spacing", - "Apply horticultural oil sprays to smother fungal growth every 7-14 days", - "For severe cases on valuable plants, use systemic fungicide containing myclobutanil or tebuconazole", - ], - prevention: [ - "Plant resistant varieties when available for the specific crop", - "Space plants adequately and prune for good air movement", - "Avoid overhead watering; use drip irrigation at soil level", - "Apply preventive sulfur spray every 7-14 days during favorable weather", - "Remove and dispose of crop debris at end of season to reduce overwintering inoculum", - ], - }, - { - name: "Root Rot (Pythium/Phytophthora)", - sciName: "Pythium spp., Phytophthora spp.", - type: "fungal", - severity: "high", - symptoms: [ - "Yellowing, wilting, and stunting of foliage despite adequate soil moisture", - "Brown, soft, mushy roots that disintegrate when touched", - "Dark brown to black discoloration of stem base at soil line", - "Gradual plant decline over days to weeks, often with interveinal chlorosis", - "Plant may fall over due to complete root system decay", - ], - causes: [ - "Soil-borne oomycete pathogens in genus Pythium or Phytophthora", - "Overwatering or poorly draining soil creating waterlogged, anaerobic conditions", - "Contaminated potting mix, garden soil, or irrigation water", - "Planting too deeply or mechanical wounding of roots and stem base", - "Warm, wet soils (60-85°F) favor rapid pathogen growth and infection", - ], - treatment: [ - "Remove and destroy severely affected plants along with surrounding soil to prevent spread", - "Improve drainage by amending heavy soils with perlite, coarse sand, or organic matter", - "Reduce watering frequency and allow soil to dry between waterings", - "Apply fungicide drench containing mefenoxam, etridiazole, or phosphorous acid for specific pathogens", - "Repot container plants with fresh sterile potting mix in a sanitized container with drainage holes", - ], - prevention: [ - "Use well-draining potting mix and containers with adequate drainage holes", - "Water only when top 1-2 inches of soil are dry to the touch", - "Avoid overwatering and standing water in saucers or drip trays", - "Sterilize pots, trays, and tools between plantings with 10% bleach solution", - "Use raised beds in areas with naturally poor drainage", - ], - }, - { - name: "Damping-Off", - sciName: "Pythium spp., Rhizoctonia solani, Fusarium spp.", - type: "fungal", - severity: "high", - symptoms: [ - "Seeds fail to germinate or seedlings fail to emerge from planting medium", - "Stems of newly emerged seedlings become thin, water-soaked, and collapse at soil line", - "Cotyledons and young leaves wilt, turn yellow, and die rapidly", - "Brownish decay visible on roots and stem base below soil surface", - "Patches of missing or fallen seedlings in seed trays or garden beds", - ], - causes: [ - "Soil-borne fungal pathogens attacking germinating seeds and succulent seedling tissue", - "Overwatering or poorly draining seed-starting medium creating waterlogged conditions", - "Contaminated potting soil or garden soil containing pathogen propagules", - "Cool soil temperatures slowing germination and seedling growth", - "Dense seeding that reduces air circulation and keeps seedling stems moist", - ], - treatment: [ - "Remove and destroy affected seedlings and surrounding medium immediately", - "Improve drainage by adding perlite or coarse sand to growing medium", - "Reduce watering frequency and allow soil surface to dry between waterings", - "Apply fungicide drench containing etridiazole or mefenoxam according to label directions", - "Increase air circulation around seedlings with a small oscillating fan", - ], - prevention: [ - "Use sterile seed-starting mix, never garden soil, for seed germination", - "Sterilize seed trays, flats, and tools with 10% bleach solution before use", - "Water from below by placing trays in water, never overhead onto seedlings", - "Provide adequate light and avoid overcrowding of seedlings in flats", - "Warm soil to 70-75°F using heat mats for optimal germination speed", - ], - }, - { - name: "Anthracnose", - sciName: "Colletotrichum spp.", - type: "fungal", - severity: "moderate", - symptoms: [ - "Circular sunken lesions on fruits, leaves, stems, and flowers", - "Dark brown to black spots with pinkish-orange spore masses in wet weather", - "Leaf spots that enlarge and coalesce, causing leaf blight and defoliation", - "Fruit rot that starts as small circular spots and enlarges, ruining marketability", - "Dieback of twigs and branches on woody plants, with canker formation", - ], - causes: [ - "Fungal pathogens in the Colletotrichum genus with broad host ranges", - "Spores splash-dispersed by rain, overhead irrigation, and wind-driven water", - "Warm humid conditions (70-85°F) with extended leaf wetness periods over 12 hours", - "Overwintering on infected plant debris, mummified fruit, and infected seeds", - ], - treatment: [ - "Prune out and destroy infected branches, stems, and fruit during dry weather", - "Apply copper fungicide or chlorothalonil at first sign of disease, repeating every 7-14 days", - "Improve air circulation through proper pruning and plant spacing", - "Remove and destroy fallen leaves, fruit, and other plant debris from around plants", - "Apply protective fungicide sprays during bloom and fruit development stages", - ], - prevention: [ - "Plant resistant varieties when available for specific crops", - "Water at soil level and avoid wetting foliage with overhead irrigation", - "Mulch around plants with 2-3 inches of organic material to prevent soil splash", - "Practice crop rotation with non-host crops for 2-3 years", - "Sanitize pruning tools between cuts with 70% alcohol solution", - ], - }, - { - name: "Gray Mold (Botrytis Blight)", - sciName: "Botrytis cinerea", - type: "fungal", - severity: "high", - symptoms: [ - "Soft, brown, water-soaked spots on leaves, stems, flowers, and fruit", - "Grayish-brown fuzzy mold growth on decaying plant tissue in humid conditions", - "Rapid spread of decay, especially on damaged or senescing plant tissue", - "Flower blight causing blossoms to turn brown and collapse", - "Large irregular lesions on fruit that become covered with gray spores", - ], - causes: [ - "Fungal pathogen Botrytis cinerea with very broad host range (200+ species)", - "Cool, humid conditions (55-70°F) with poor air circulation", - "Entry through wounds, senescent flowers, or mechanical damage", - "Overhead irrigation and crowding that keep foliage wet for extended periods", - "Spores produced prolifically and spread by air currents and water splash", - ], - treatment: [ - "Remove and destroy all infected plant parts and debris immediately", - "Improve air circulation through spacing, pruning, and ventilation", - "Reduce humidity by watering at soil level and watering early in day", - "Apply fungicide containing chlorothalonil, thiophanate-methyl, or fenhexamid", - "Avoid working among wet plants to prevent spore spread", - ], - prevention: [ - "Space plants adequately for air circulation", - "Water at soil level early in the day so foliage dries before nightfall", - "Remove spent flowers and senescing leaves promptly", - "Avoid high nitrogen fertilization that promotes lush, susceptible growth", - "Use preventive fungicide sprays during extended cool, wet weather", - ], - }, - { - name: "Rust", - sciName: "Puccinia spp., Uromyces spp., Phragmidium spp.", - type: "fungal", - severity: "moderate", - symptoms: [ - "Bright orange, yellow, reddish-brown, or dark brown pustules on leaf undersides", - "Corresponding yellow chlorotic spots on upper leaf surfaces above pustules", - "Severe infections cause leaf curling, distortion, and premature defoliation", - "Stems, petioles, and even fruit may develop pustules in heavy infections", - "Reduced plant vigor, stunting, and significant yield loss", - ], - causes: [ - "Obligate parasitic rust fungi requiring living plant tissue to survive", - "Spores dispersed by wind over long distances from infected plants", - "Free moisture in the form of dew or rain required for spore germination on leaf surface", - "Some rust fungi require two different host species to complete their life cycle", - "Moderate temperatures (60-75°F) with high humidity favor disease development", - ], - treatment: [ - "Remove and destroy infected leaves and plant parts at first sign of pustules", - "Apply sulfur or copper fungicide as a protectant every 7-14 days", - "Use systemic fungicide containing myclobutanil, tebuconazole, or azoxystrobin for existing infections", - "Improve air circulation to reduce leaf wetness duration", - "Avoid overhead watering to keep foliage dry", - ], - prevention: [ - "Plant resistant varieties when available", - "Remove alternate hosts (e.g., junipers for cedar-apple rust) when possible", - "Space plants for good air circulation and rapid leaf drying", - "Water early in the day so foliage dries before nightfall", - "Apply preventive fungicide in spring if rust was severe the previous season", - ], - }, - { - name: "Leaf Spot (Septoria/Cercospora)", - sciName: "Septoria spp., Cercospora spp.", - type: "fungal", - severity: "low", - symptoms: [ - "Small circular to irregular spots on leaves with defined dark margins", - "Spots have tan, gray, or light brown centers with purplish-black borders", - "Tiny black specks (pycnidia) visible in center of spots under magnification", - "Spots may coalesce causing large dead areas and premature leaf drop", - "Disease progresses from lower leaves upward, reducing photosynthetic area", - ], - causes: [ - "Host-specific fungal pathogens in Septoria or Cercospora genera", - "Spores splashed onto lower leaves during rain or overhead watering", - "High humidity and poor air circulation around plants", - "Infected plant debris left in garden from previous season", - "Fungal propagules survive in soil and on infected seeds", - ], - treatment: [ - "Remove and destroy infected lower leaves as soon as leaf spots appear", - "Apply copper fungicide, chlorothalonil, or sulfur spray every 7-14 days", - "Improve air circulation by thinning dense foliage and pruning lower branches", - "Water at soil level using drip irrigation to keep foliage dry", - "Clean up all fallen leaves and debris around plants", - ], - prevention: [ - "Space plants adequately for good air movement", - "Avoid overhead watering; use drip irrigation or soaker hoses", - "Mulch around plants with 2-3 inches of organic material to reduce soil splash", - "Remove and dispose of all plant debris at end of season", - "Rotate crops to prevent pathogen buildup in garden soil", - ], - }, - { - name: "Bacterial Leaf Spot", - sciName: "Xanthomonas spp., Pseudomonas spp.", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Small water-soaked spots on leaves that enlarge and turn brown or black", - "Angular lesions bounded by leaf veins, giving a geometric appearance", - "Yellow halos surrounding individual leaf spots", - "Leaf drop and defoliation in severe infections", - "Lesions may also appear on stems, fruit, and flowers", - ], - causes: [ - "Bacterial pathogens in Xanthomonas or Pseudomonas genera", - "Spread by rain splash, irrigation water, and contaminated hands and tools", - "Warm temperatures (75-90°F) with high humidity and leaf wetness", - "Bacteria enter through stomata or small wounds in leaf tissue", - "Overwinter on infected seeds, plant debris, and volunteer plants", - ], - treatment: [ - "Remove and destroy heavily infected leaves and plants", - "Apply copper-based bactericide at first sign of disease", - "Improve air circulation through proper spacing and pruning", - "Avoid overhead irrigation; use drip irrigation", - "Rotate with non-host crops for 2-3 years", - ], - prevention: [ - "Use certified disease-free seed and pathogen-free transplants", - "Apply fixed copper sprays preventively during favorable weather", - "Avoid working among wet plants to prevent bacterial spread", - "Sterilize stakes, cages, and tools between seasons", - "Practice crop rotation with non-host plant families", - ], - }, - { - name: "Mosaic Virus", - sciName: "Multiple potyviruses, cucumoviruses, tobamoviruses", - type: "viral", - severity: "high", - symptoms: [ - "Mottled light and dark green or yellow-green mosaic pattern on leaves", - "Leaf puckering, curling, distortion, or unusual narrowing of leaf blades", - "Stunted plant growth with shortened internodes and reduced vigor", - "Yellowing along leaf veins (vein clearing) or intervenal chlorosis", - "Fruit may have mottling, streaking, ringspots, or reduced size and quality", - ], - causes: [ - "Virus particles transmitted by insect vectors including aphids, thrips, and whiteflies", - "Mechanical transmission through contaminated hands, tools, and clothing", - "Use of infected propagation material including cuttings, tubers, bulbs, and seeds", - "Virus survival in perennial weed hosts and wild reservoir plants", - ], - treatment: [ - "No cure available — remove and destroy infected plants immediately upon detection", - "Decontaminate tools, pots, and work surfaces with 10% bleach or trisodium phosphate", - "Wash hands thoroughly with soap and water after handling infected plants", - "Control insect vectors using reflective mulches, row covers, and appropriate insecticides", - "Remove weeds and alternate host plants that may serve as virus reservoirs", - ], - prevention: [ - "Purchase certified virus-free seed and transplants from reputable sources", - "Use reflective plastic mulches to repel aphids during early growth", - "Isolate new plants for a 2-week quarantine period before introducing to garden", - "Remove and destroy any symptomatic plants promptly", - "Rotate out of susceptible crops for at least 2 growing seasons", - ], - }, - { - name: "Wilt (Fusarium or Verticillium)", - sciName: "Fusarium oxysporum, Verticillium dahliae", - type: "fungal", - severity: "high", - symptoms: [ - "Yellowing and wilting of lower leaves, progressing upward on one side of plant", - "Vascular tissue in stem shows brown or dark discoloration when cut lengthwise", - "Stunting and overall plant decline with reduced leaf size and vigor", - "Wilting that is more severe during hot afternoons with some recovery overnight", - "Eventual death of the entire plant as vascular system becomes blocked", - ], - causes: [ - "Soil-borne fungal pathogens invading through root tips and wounds", - "Fungi survive in soil for many years as resistant structures", - "Spread by contaminated soil, water, tools, and infected transplants", - "Warm soil temperatures (75-85°F) favor Fusarium; cooler soils (70-75°F) favor Verticillium", - "Root-knot nematode damage increases susceptibility to wilt pathogens", - ], - treatment: [ - "Remove and destroy infected plants including as much root system as possible", - "Solarize contaminated soil by covering with clear plastic for 4-6 weeks in summer", - "Do not replant susceptible crops in infested soil for 5-7 years", - "No fungicide is effective once plants show symptom", - "Graft susceptible varieties onto resistant rootstocks", - ], - prevention: [ - "Plant resistant varieties (look for F, V, or F1/V1 resistance codes)", - "Practice long crop rotation (5-7 years) with non-host crops", - "Use raised beds to improve soil drainage", - "Control root-knot nematodes that predispose plants to wilts", - "Sterilize garden tools and avoid moving contaminated soil", - ], - }, - { - name: "Root-Knot Nematode", - sciName: "Meloidogyne spp.", - type: "environmental", - severity: "high", - symptoms: [ - "Stunted plant growth with yellowing and wilting during hot weather", - "Swollen galls or knots on root system visible when plants are carefully dug up", - "Plants fail to respond to water and fertilizer applications", - "Reduced yield with smaller fruit, tubers, or grain heads", - "Root system becomes deformed, branched, and unable to take up water and nutrients", - ], - causes: [ - "Microscopic roundworms (nematodes) in the genus Meloidogyne feeding on root tissue", - "Introduction through infected plants, soil on tools, or contaminated irrigation water", - "Nematodes spread by water movement, equipment, and infected plant material", - "Warm sandy soils with low organic matter favor nematode reproduction and damage", - "Continuous cropping of susceptible hosts increases population levels", - ], - treatment: [ - "Remove and destroy severely infected plants including entire root system and surrounding soil", - "Solarize soil by covering with clear plastic tarp for 6-8 weeks during hottest summer months", - "Incorporate large quantities of organic matter to promote beneficial soil microorganisms", - "Plant marigolds (Tagetes erecta or T. patula) as a biofumigant cover crop for one season", - "Apply neem-based soil amendments to suppress nematode populations", - ], - prevention: [ - "Use certified nematode-free transplants grown in sterile potting mix", - "Practice crop rotation with non-host crops (grains, grasses) for 3-5 years", - "Choose resistant plant varieties when available (look for N designation)", - "Solarize soil before planting in known infested areas", - "Clean soil off all tools and equipment between garden areas", - ], - }, - { - name: "Sunscald", - sciName: "Physiological disorder — heat and light stress", - type: "environmental", - severity: "low", - symptoms: [ - "Bleached, papery white or tan patches on fruits and leaves exposed to intense direct sunlight", - "Soft, sunken, wrinkled tissue on the sun-exposed side of fruit", - "Affected tissue becomes thin, dry, and may crack or split open", - "Secondary fungal or bacterial infection often follows sunscald damage", - "On trees: cracked, peeling, or sunken bark on south or southwest-facing trunks", - ], - causes: [ - "Intense direct sunlight and high temperatures causing tissue damage and cell death", - "Insufficient foliage cover to shade developing fruit", - "Heavy or late-season pruning exposing previously shaded fruit to direct sun", - "Removal of shade from nearby trees, structures, or row covers", - "Sudden transplanting to full sun without proper hardening off", - ], - treatment: [ - "Provide temporary shade using shade cloth (30-50%), row cover, or lattice", - "Avoid removing leaves that provide natural shade to fruit during hot periods", - "Affected fruit will not heal — remove sunburned fruit to reduce plant stress", - "Apply 2-3 inches of organic mulch to moderate soil temperature and moisture", - "For tree trunk sunscald, wrap trunk with white commercial tree wrap or paint with diluted white latex paint", - ], - prevention: [ - "Maintain adequate foliage to shade fruit (avoid heavy pruning before hot weather)", - "Plant with proper spacing to allow natural canopy shade development", - "Use shade cloth during extreme heat events", - "Apply white tree wrap or whitewash to young tree trunks in sunny climates", - "Gradually acclimate transplants to full sun over 7-10 days", - ], - }, - { - name: "Blossom End Rot", - sciName: "Physiological disorder — calcium deficiency in fruit", - type: "environmental", - severity: "moderate", - symptoms: [ - "Small water-soaked spot at blossom end of fruit that enlarges and darkens over time", - "Brown to black sunken leathery lesion on the bottom (distal end) of fruit", - "Lesion may become colonized by secondary fungi, turning black and fuzzy", - "Affected area grows as the fruit expands, ruining marketability", - "Multiple fruits on the same plant are often affected simultaneously", - ], - causes: [ - "Calcium deficiency in developing fruit due to inconsistent water availability", - "Fluctuating soil moisture levels preventing calcium uptake and transport through plant", - "Excessive nitrogen fertilization promoting rapid foliage growth at expense of fruit", - "Root damage, restriction, or poor soil structure limiting root exploration and calcium absorption", - "High soil salinity or ammonium-based fertilizers interfering with calcium uptake", - ], - treatment: [ - "Maintain consistent soil moisture with regular watering of 1-2 inches per week", - "Apply 2-3 inches of organic mulch to moderate soil moisture fluctuations", - "Test soil pH and adjust to 6.2-6.8 for optimal calcium availability", - "Switch to calcium-based fertilizer (calcium nitrate) instead of high-nitrogen formulas", - "Remove affected fruit so plant redirects energy to healthy developing fruit", - ], - prevention: [ - "Water consistently using drip irrigation with timer for regularity", - "Maintain even soil moisture with generous organic mulch layer", - "Test soil pH before planting and amend with lime if below 6.0", - "Avoid high-nitrogen fertilizers that promote foliage over fruit development", - "Ensure adequate rooting depth by preparing soil to 12-18 inches deep", - ], - }, - { - name: "Nutrient Deficiency (General)", - sciName: "Various macro and micronutrient deficiencies", - type: "environmental", - severity: "low", - symptoms: [ - "Chlorosis (yellowing) of leaves, often in specific patterns depending on deficient nutrient", - "Stunted growth with reduced leaf size and shortened internodes", - "Poor fruit set, flower drop, or small misshapen fruit", - "Leaf margin necrosis (scorching) or interveinal chlorosis", - "Overall reduced vigor and delayed maturity", - ], - causes: [ - "Insufficient levels of essential plant nutrients in soil or growing medium", - "Soil pH outside optimal range for nutrient availability (most nutrients available at pH 6.0-7.0)", - "Poor root health limiting nutrient uptake despite adequate soil levels", - "Excessive leaching of nutrients from sandy soils or overwatering", - "Soil compaction or poor aeration restricting root growth", - ], - treatment: [ - "Conduct professional soil test to identify specific nutrient deficiencies and pH", - "Apply balanced fertilizer appropriate for the specific crop and identified deficiencies", - "Adjust soil pH using lime (to raise) or sulfur (to lower) based on test results", - "Use foliar nutrient sprays for rapid correction of micronutrient deficiencies", - "Improve soil organic matter content through compost incorporation", - ], - prevention: [ - "Test soil before planting and amend to recommended nutrient levels", - "Use balanced slow-release fertilizer according to crop requirements", - "Maintain proper soil pH for the specific crop being grown", - "Incorporate compost or well-rotted manure annually", - "Practice crop rotation to prevent depletion of specific nutrients", - ], - }, - { - name: "Overwatering Damage (Edema)", - sciName: "Physiological disorder — excess water uptake", - type: "environmental", - severity: "low", - symptoms: [ - "Small blister-like bumps or corky growths on undersides of leaves", - "White to tan raised lesions that become brown and corky with age", - "Leaf curling, yellowing, and premature leaf drop", - "Root decay and foul odor from waterlogged soil", - "Wilting despite wet soil due to damaged root system", - ], - causes: [ - "Excessive soil moisture preventing proper oxygen exchange at roots", - "Poorly draining soil or containers without drainage holes", - "Watering too frequently without allowing soil to dry between waterings", - "High humidity combined with cool temperatures reducing transpiration", - "Compact soil structure that holds water for extended periods", - ], - treatment: [ - "Allow soil to dry out completely before watering again", - "Improve drainage by repotting with fresh well-draining mix or amending garden soil", - "Remove severely damaged leaves to reduce water demand", - "Increase air circulation around plants with fans or spacing", - "Reduce watering frequency appropriate for the specific plant species and season", - ], - prevention: [ - "Use well-draining potting mix and containers with drainage holes", - "Water only when top 1-2 inches of soil are dry", - "Choose plants appropriate for the existing light and humidity conditions", - "Use pots with good drainage and avoid letting plants sit in standing water", - "Learn specific watering needs for each plant species", - ], - }, - { - name: "Herbicide Injury", - sciName: "Chemical injury — herbicide drift or residue", - type: "environmental", - severity: "moderate", - symptoms: [ - "Cupping, curling, or twisting of leaves and new growth", - "Yellowing or bleaching of leaf veins and interveinal tissue", - "Stunted growth with thickened, brittle stems and leaves", - "Leaf distortion with narrow, strappy appearance (hormone herbicide damage)", - "Reduced fruit set, flower abortion, or misshapen fruit", - ], - causes: [ - "Drift of herbicide spray from nearby lawns, fields, or right-of-way treatments", - "Volatilization of hormone-type herbicides (2,4-D, dicamba) moving as vapor", - "Herbicide residues in compost, manure, or contaminated irrigation water", - "Contaminated spray equipment used for fertilizer or pesticide applications", - "Residual herbicides in soil from previous growing season", - ], - treatment: [ - "Remove severely affected plant parts that show distortion", - "Water deeply to help leach soil-active herbicides from root zone", - "Apply activated charcoal to soil surface to absorb certain herbicides", - "Support plant health with proper water and balanced fertilizer", - "Most herbicide injuries are not fatal — plants often recover if new growth is unaffected", - ], - prevention: [ - "Do not apply herbicides near desirable plants on windy days", - "Use dedicated spray equipment for herbicides, separate from other chemicals", - "Use low-volatility herbicide formulations when possible", - "Maintain buffer zones between treated areas and gardens", - "Avoid using herbicide-treated grass clippings in garden compost", - ], - }, - { - name: "Sooty Mold", - sciName: "Capnodium spp., various saprophytic fungi", - type: "fungal", - severity: "low", - symptoms: [ - "Black, powdery or crusty fungal growth coating upper surfaces of leaves and stems", - "Growth is superficial and can be wiped off with a damp cloth", - "Underneath sooty mold, leaves may be sticky from honeydew secretions", - "Reduced photosynthesis due to blocked sunlight on leaf surfaces", - "Presence of ants farming sap-feeding insects that produce honeydew", - ], - causes: [ - "Fungi growing on honeydew produced by sap-feeding insects (aphids, scale, whiteflies, mealybugs)", - "Insect infestation on plants providing continuous honeydew supply", - "Fungal spores airborne and germinating on honeydew-coated surfaces", - "Underlying insect problem not being addressed", - ], - treatment: [ - "Wash sooty mold off leaves with a strong spray of water or mild soap solution", - "Identify and control the underlying sap-feeding insect infestation", - "Apply horticultural oil or insecticidal soap to control insects", - "For heavy mold, use neem oil spray that both smothers mold and controls insects", - "Prune out heavily infested branches to reduce insect populations", - ], - prevention: [ - "Monitor plants regularly for sap-feeding insects", - "Control ant populations that protect honeydew-producing insects", - "Maintain plant health to resist insect infestations", - "Encourage beneficial insects (ladybugs, lacewings) that prey on aphids and scale", - "Inspect new plants for insects before bringing them into garden", - ], - }, - { - name: "Canker (Stem/Branch)", - sciName: - "Various fungal and bacterial genera including Cytospora, Botryosphaeria, Nectria, Pseudomonas", - type: "fungal", - severity: "high", - symptoms: [ - "Sunken, discolored, cracked, or dead areas (cankers) on stems, branches, or trunk", - "Bark may split open around infected area revealing discolored wood underneath", - "Reddish or amber-colored gum or ooze exuding from cankers on stone fruits", - "Dieback of branches, shoots, or entire limbs beyond the canker location", - "Leaf yellowing, wilting, or premature fall coloring on affected branches", - ], - causes: [ - "Fungal or bacterial pathogens entering through wounds in bark or branch tissue", - "Mechanical injury from pruning cuts, lawnmowers, string trimmers, or weather damage", - "Environmental stress including drought, frost cracks, sunscald, or nutrient deficiency", - "Infected pruning tools spreading disease from tree to tree between cuts", - "Insect damage creating entry points for canker pathogens", - ], - treatment: [ - "Prune out infected branches 6-12 inches below visible canker symptoms during dry weather", - "Sterilize all pruning tools with 70% alcohol or 10% bleach solution between every cut", - "For trunk cankers on valuable trees, excise infected bark down to healthy wood with a sharp knife", - "Improve tree vigor through proper watering, fertilization, and mulching", - "No chemical cure exists once canker is established — prevent stress to limit spread", - ], - prevention: [ - "Avoid wounding bark near soil line with lawnmowers and string trimmers", - "Prune during dormant season to reduce disease spread", - "Make clean pruning cuts at branch collar, not flush with trunk", - "Maintain tree health through proper watering during drought", - "Mulch around trees keeping mulch 2-3 inches away from trunk", - ], - }, - { - name: "Bacterial Soft Rot", - sciName: "Erwinia carotovora (Pectobacterium carotovorum), Pseudomonas spp.", - type: "bacterial", - severity: "high", - symptoms: [ - "Soft, mushy, water-soaked rot of fleshy tissue (tubers, bulbs, stems, fruit)", - "Rot spreads rapidly in warm humid conditions, often within hours", - "Foul odor from decomposing tissue due to secondary bacteria", - "Tissue becomes slimy and collapses into a wet mass", - "Leaves above rot may wilt and turn yellow", - ], - causes: [ - "Bacteria entering through wounds, mechanical damage, or insect injury", - "Warm temperatures (75-90°F) with high humidity accelerate decay", - "Excess moisture on plant surfaces and in storage", - "Bacteria survive in infected plant debris, soil, and contaminated water", - "Poor ventilation and overcrowding in storage", - ], - treatment: [ - "Remove and destroy all infected plant parts immediately", - "Discard affected stored produce and improve storage ventilation", - "Avoid harvesting or handling plants when they are wet", - "Apply copper-based bactericide as a protective spray on surrounding plants", - "Cure potatoes and other tubers properly before storage (50-60°F for 10-14 days)", - ], - prevention: [ - "Handle plants carefully to minimize bruising and wounds during harvest", - "Harvest only when temperatures are cool and plants are dry", - "Provide adequate spacing for air circulation", - "Clean and disinfect storage areas before use", - "Avoid over-application of nitrogen fertilizer", - ], - }, - { - name: "Downy Mildew (Generic)", - sciName: "Peronospora spp., Plasmopara spp., Bremia spp.", - type: "fungal", - severity: "high", - symptoms: [ - "Pale green to yellow angular spots on upper leaf surfaces bounded by veins", - "White to grayish-purple fuzzy growth on leaf undersides beneath spots", - "Leaf spots turn brown and necrotic as tissue dies", - "Rapid defoliation under favorable conditions (cool, wet weather)", - "Infected flowers and fruit may develop sporulation and rot", - ], - causes: [ - "Obligate oomycete pathogens with specific host plant preferences", - "Spores spread by wind and water splash from infected plants", - "Cool temperatures (55-70°F) with high humidity and free leaf moisture", - "Overhead irrigation and dense plantings that hold moisture", - "Overwinters in infected plant debris and on volunteer plants", - ], - treatment: [ - "Remove and destroy infected leaves and plant parts at first sign", - "Apply fungicide containing mefenoxam, chlorothalonil, or mancozeb", - "Improve air circulation through spacing and pruning", - "Water at soil level early in the day", - "Rotate fungicides with different modes of action to prevent resistance", - ], - prevention: [ - "Plant resistant varieties when available", - "Space plants adequately for air movement", - "Avoid overhead watering; use drip irrigation", - "Apply preventive fungicide when conditions favor disease", - "Remove crop debris at end of season", - ], - }, - { - name: "Viral Leaf Curl", - sciName: "Geminiviridae (Begomovirus spp.), various leaf curl viruses", - type: "viral", - severity: "high", - symptoms: [ - "Leaves curl upward or downward with thickened, distorted blades", - "Yellow mosaic or chlorotic patterns between leaf veins", - "Stunted growth with shortened internodes and bushy appearance", - "Reduced fruit set with small, misshapen fruit", - "Leaf veins may become swollen or enations (leaf-like outgrowths) form on veins", - ], - causes: [ - "Geminiviruses transmitted by whiteflies (Bemisia tabaci) in a persistent manner", - "High whitefly populations in warm climates favor rapid spread", - "Virus survives in infected weed hosts and volunteer crop plants", - "Movement of infected plant material introduces virus to new areas", - ], - treatment: [ - "Remove and destroy infected plants immediately upon detection", - "Control whitefly populations with insecticides and yellow sticky traps", - "Use reflective mulches (aluminum-coated) to repel whiteflies", - "No cure for infected plants — focus on vector control", - "Remove weed hosts that serve as virus reservoirs", - ], - prevention: [ - "Use certified virus-free transplants from reputable sources", - "Install reflective plastic mulch before planting", - "Use insect-proof row covers over young plants", - "Maintain weed-free zone around crop area", - "Practice crop isolation from known infected areas", - ], - }, - { - name: "Lesion Nematode", - sciName: "Pratylenchus spp.", - type: "environmental", - severity: "moderate", - symptoms: [ - "Irregular brown to black lesions on root surfaces visible when washed", - "Stunted plant growth with yellowing foliage that wilts in heat", - "Reduced root system with darkened, decayed areas", - "Plants fail to respond to water and fertilizer", - "Reduced yield and poor quality harvest", - ], - causes: [ - "Migratory endoparasitic nematodes feeding and reproducing within root tissue", - "Nematodes move through soil to infect new roots", - "Spread by contaminated soil, plants, and equipment", - "Continuous cropping of susceptible hosts increases populations", - ], - treatment: [ - "Remove and destroy infected plants including entire root system", - "Solarize soil with clear plastic for 6-8 weeks in summer", - "Incorporate organic matter to promote beneficial microorganisms", - "Plant nematode-suppressive cover crops (marigolds, rapeseed)", - "Apply neem-based soil amendments", - ], - prevention: [ - "Use certified nematode-free planting material", - "Practice crop rotation with non-host crops for 2-3 years", - "Clean soil off equipment between fields", - "Maintain high organic matter levels in soil", - "Use resistant varieties when available", - ], - }, - { - name: "Wood Rot (Decay)", - sciName: "Various basidiomycetes including Fomes, Armillaria, Ganoderma spp.", - type: "fungal", - severity: "high", - symptoms: [ - "Conks (bracket fungi) or mushroom-like fruiting bodies on trunks and branches", - "Wood becomes soft, spongy, stringy, or crumbly when probed", - "Branch dieback and reduced leaf size and vigor", - "Trunk may show sunken or cracked bark areas", - "Tree may fall or break during storms due to structural weakness", - ], - causes: [ - "Wood-decay fungi entering through wounds in bark or broken branches", - "Poor pruning cuts that fail to heal properly", - "Advanced tree age and declining vigor", - "Soil compaction and root damage limiting tree health", - "Prolonged drought or flooding stress predisposing trees to infection", - ], - treatment: [ - "Remove dead and dying branches promptly with proper pruning cuts", - "Remove loose bark around decayed areas to expose to air drying", - "For valuable trees, consult a certified arborist for cabling and support", - "Remove and destroy severely infected trees that pose a safety hazard", - "No chemical treatment can cure existing wood rot", - ], - prevention: [ - "Prune properly at branch collar, leaving no stubs", - "Avoid wounding trunks with lawn equipment", - "Maintain tree vigor with proper watering during drought", - "Remove declining trees before they become safety hazards", - "Plant trees suited to the site conditions", - ], - }, - { - name: "Physiological Leaf Scorch", - sciName: "Physiological disorder — environmental stress", - type: "environmental", - severity: "low", - symptoms: [ - "Brown, dry, dead tissue at leaf margins and tips", - "Tissue death progresses inward between leaf veins", - "Symptoms most severe on side exposed to wind or sun", - "Premature leaf drop in late summer", - "More pronounced on newly transplanted or shallow-rooted plants", - ], - causes: [ - "Inadequate water uptake to meet transpiration demand", - "Hot, dry, or windy weather increasing water loss from leaves", - "Root damage, restricted root zone, or root disease limiting water absorption", - "Reflected heat from buildings, pavement, or walls", - "Salt damage from deicing salts or excessive fertilization", - ], - treatment: [ - "Deep water at root zone during dry periods (1-2 inches per week)", - "Apply 2-4 inches of organic mulch to conserve soil moisture and cool roots", - "Provide temporary shade during extreme heat events", - "Prune out severely scorched branches", - "Avoid fertilization during heat stress", - ], - prevention: [ - "Water deeply and regularly during dry weather", - "Mulch around plants to moderate soil temperature and moisture", - "Plant in locations protected from harsh wind and reflected heat", - "Choose plants adapted to local climate conditions", - "Avoid excessive nitrogen fertilization", - ], - }, -]; - -// ─── Family-specific disease templates ────────────────────────────────────── - -export interface FamilyTemplates { - families: string[]; // Plant families this applies to - templates: DiseaseSpec[]; // Disease templates specific to these families -} - -export const FAMILY_TEMPLATES: FamilyTemplates[] = [ - // ── Solanaceae (Nightshade) ────────────────────────────────────────── - { - families: ["Solanaceae"], - templates: [ - { - name: "Early Blight", - sciName: "Alternaria solani", - type: "fungal", - severity: "moderate", - symptoms: [ - "Dark brown to black spots with concentric rings (target-board pattern) on lower leaves", - "Yellowing of leaf tissue surrounding spots", - "Premature defoliation starting from bottom of plant", - "Dark sunken lesions on stems and fruit near soil line", - "Reduced fruit size and quality", - ], - causes: [ - "Fungus overwinters in infected plant debris in soil", - "Warm temperatures (75-85°F) with high humidity and leaf wetness", - "Spores spread by splashing rain and overhead irrigation water", - "Nutrient deficiencies, particularly low potassium, weaken plant resistance", - ], - treatment: [ - "Remove and destroy all severely infected lower leaves immediately — do not compost", - "Apply copper-based fungicide or chlorothalonil spray every 7-14 days", - "Mulch around plants with 2-3 inches of straw to prevent soil splash", - "Improve air circulation through pruning, staking, and proper spacing", - "Switch to drip irrigation to keep foliage dry", - ], - prevention: [ - "Practice 2-3 year crop rotation with non-Solanaceae crops", - "Water at soil level using drip irrigation, never overhead", - "Space plants 24-36 inches apart for adequate air circulation", - "Choose resistant varieties when available", - "Remove all plant debris at end of season and sanitize stakes and cages", - ], - }, - { - name: "Late Blight", - sciName: "Phytophthora infestans", - type: "fungal", - severity: "critical", - symptoms: [ - "Large irregular dark green to black water-soaked lesions on leaves", - "White fuzzy fungal growth on undersides of leaves in humid conditions", - "Rapid browning and death of entire leaves and stems within days", - "Firm dark brown greasy-looking rot on fruit that penetrates deep into flesh", - "Grayish-white mold growth on stems and petioles", - ], - causes: [ - "Oomycete pathogen Phytophthora infestans, cause of the Irish Potato Famine", - "Cool wet weather (60-70°F) with prolonged leaf wetness periods", - "Spores blown from infected potato fields or neighboring gardens over many miles", - "Infected seed potatoes and tomato transplants from infected sources", - "Overhead irrigation extending leaf wetness periods beyond 12 hours", - ], - treatment: [ - "Immediately remove and destroy all infected plant material in sealed bags", - "Apply mancozeb or copper-based fungicide as emergency treatment every 5-7 days", - "Harvest any unaffected fruit immediately and cure indoors at 85°F", - "Reduce humidity around plants through improved air circulation and pruning", - "In severe outbreaks, destroy entire crop to prevent regional spread to other gardens", - ], - prevention: [ - "Plant resistant varieties such as 'Mountain Merit', 'Defiant', or 'Iron Lady'", - "Avoid overhead watering entirely — use drip irrigation", - "Do not plant tomatoes near potato fields or gardens with potatoes", - "Monitor local late blight alerts from extension services", - "Apply preventive fungicide sprays starting at flowering in high-risk areas", - ], - }, - { - name: "Septoria Leaf Spot", - sciName: "Septoria lycopersici", - type: "fungal", - severity: "moderate", - symptoms: [ - "Small circular spots (1/16-1/4 inch) with dark brown borders and tan/gray centers", - "Tiny black specks (pycnidia) visible in center of spots under magnification", - "Yellowing and dropping of older leaves, progressing upward on plant", - "Heavy defoliation leaving only the top few leaves on severely affected plants", - "Reduced fruit set and smaller fruit size due to lost photosynthetic capacity", - ], - causes: [ - "Hot (75-85°F), humid weather with frequent rain or overhead irrigation", - "Fungal spores splashing from soil where infected debris overwintered", - "Dense plantings that keep foliage wet and reduce air circulation", - "Working among wet plants and spreading spores on hands and tools", - ], - treatment: [ - "Remove and destroy infected lower leaves immediately — start from bottom and work up", - "Apply copper fungicide or chlorothalonil spray every 7-14 days, covering both leaf surfaces", - "Mulch heavily (3-4 inches) around base to prevent soil splash onto leaves", - "Improve air circulation through staking, pruning, and adequate spacing", - "Apply broad-spectrum fungicide containing myclobutanil for severe infections", - ], - prevention: [ - "Rotate crops — do not plant tomatoes, peppers, or potatoes in same bed for 2-3 years", - "Use drip irrigation and avoid wetting foliage", - "Space plants 24-36 inches apart for adequate air circulation", - "Remove all plant debris and sanitize tools at end of season", - "Choose resistant varieties such as 'Juliet', 'Defiant', or 'Phoenix'", - ], - }, - { - name: "Bacterial Spot", - sciName: "Xanthomonas euvesicatoria", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Small, dark, water-soaked spots on leaves that turn brown with yellow halos", - "Irregular, raised, scabby spots on fruit that may have a cracked surface", - "Leaf spots coalesce causing large dead areas and defoliation", - "Spots on stems and petioles similar to those on leaves", - "Severe infection reduces yield and fruit quality", - ], - causes: [ - "Bacterium Xanthomonas euvesicatoria infecting through natural openings and wounds", - "Spread by rain splash, overhead irrigation, and contaminated hands and tools", - "Warm temperatures (75-90°F) with high humidity favor rapid disease development", - "Bacteria survive on infected seed, plant debris, and volunteer plants", - ], - treatment: [ - "Remove and destroy heavily infected plants — do not compost", - "Apply copper-based bactericide at first sign of disease, repeating every 7-10 days", - "Avoid overhead irrigation; water at soil level with drip irrigation", - "Improve air circulation through staking and pruning", - "Rotate with non-host crops for at least 2 years", - ], - prevention: [ - "Purchase certified disease-free seed and treated seed when available", - "Use disease-free transplants from reputable sources", - "Apply copper spray preventively during favorable weather periods", - "Avoid working among wet plants when foliage is wet", - "Control solanaceous weeds that may harbor the bacteria", - ], - }, - { - name: "Bacterial Wilt", - sciName: "Ralstonia solanacearum", - type: "bacterial", - severity: "critical", - symptoms: [ - "Sudden wilting of lower leaves followed by rapid wilting of entire plant", - "Vascular tissue in stem shows brown discoloration when cut crosswise", - "White or yellowish bacterial ooze exuding from cut stem when placed in water", - "Plant collapse within days of first symptom appearance", - "No leaf yellowing precedes wilting — leaves remain green initially", - ], - causes: [ - "Bacterium Ralstonia solanacearum (formerly Pseudomonas solanacearum)", - "Bacteria enter through root tips and wounds in the root system", - "Spread through contaminated soil, irrigation water, and infected transplants", - "Warm soils (80-95°F) and high moisture levels favor disease", - "Bacteria survive for years in soil and infected plant debris", - ], - treatment: [ - "Remove and destroy infected plants immediately — bag and remove from garden", - "Solarize contaminated soil by covering with clear plastic for 4-6 weeks in summer", - "Do not replant susceptible crops in infected area for 3-5 years", - "No chemical cure exists once plants are infected", - "Sterilize all tools and stakes with 10% bleach solution", - ], - prevention: [ - "Use certified disease-free transplants", - "Practice long crop rotation (3-5 years) with non-Solanaceae crops", - "Plant in well-drained soil and avoid overwatering", - "Control root-knot nematodes that create entry wounds for bacteria", - "Avoid moving soil from infected areas to clean areas", - ], - }, - { - name: "Tobacco Mosaic Virus (TMV)", - sciName: "Tobacco mosaic virus", - type: "viral", - severity: "high", - symptoms: [ - "Light and dark green mottled mosaic pattern on leaves", - "Leaf puckering, distortion, and stunted growth", - "Yellowing along leaf veins in early stages", - "Fruit may develop mottling, uneven ripening, and reduced size", - "Overall stunting and reduced yield", - ], - causes: [ - "Tobacco mosaic virus — highly stable virus with very broad host range", - "Mechanical transmission through contaminated hands, tools, and clothing", - "Virus survives in cured tobacco products and infected plant debris", - "No insect vector required — spread entirely by mechanical contact", - "Virus remains infectious for decades in dried plant material", - ], - treatment: [ - "No cure — remove and destroy infected plants as soon as detected", - "Decontaminate tools and hands with 10% bleach or trisodium phosphate solution", - "Wash hands thoroughly with soap after handling plants, especially after smoking", - "Remove and destroy all infected plant material promptly", - "Do not compost infected plants — virus survives in compost", - ], - prevention: [ - "Wash hands thoroughly with soap and water before handling plants", - "Never smoke or use tobacco products near susceptible plants", - "Use dedicated tools for handling plants and sanitize regularly", - "Purchase certified virus-free seed and transplants", - "Remove solanaceous weeds that may serve as virus reservoirs", - ], - }, - { - name: "Bacterial Canker", - sciName: "Clavibacter michiganensis subsp. michiganensis", - type: "bacterial", - severity: "high", - symptoms: [ - "Wilting of leaflets on one side of leaf or one side of plant", - "Brown streaks on stems and petioles that develop into cankers", - "Bird's-eye spots on fruit — small white spots with dark brown centers", - "Yellowing and browning of leaf margins (scorched appearance)", - "Vascular tissue in stem shows yellowish-brown discoloration", - ], - causes: [ - "Bacterium Clavibacter michiganensis subsp. michiganensis", - "Entering through wounds in roots, stems, and leaves", - "Spread by contaminated seed, transplants, and tools", - "Rain splash and overhead irrigation spread bacteria", - "Warm temperatures (75-85°F) favor disease development", - ], - treatment: [ - "Remove and destroy infected plants immediately", - "Disinfect all tools, stakes, and cages with 10% bleach or 70% alcohol", - "No effective chemical treatment once plants are infected", - "Rotate with non-host crops for 3-5 years", - "Use copper sprays preventively on surrounding healthy plants", - ], - prevention: [ - "Use certified disease-free seed (hot water treated or from reputable source)", - "Purchase transplants only from reputable sources", - "Practice 3-year crop rotation", - "Avoid overhead irrigation", - "Disinfect tools regularly, especially when pruning", - ], - }, - ], - }, - - // ── Cucurbitaceae (Gourd family) ────────────────────────────────────── - { - families: ["Cucurbitaceae"], - templates: [ - { - name: "Powdery Mildew (Cucurbits)", - sciName: "Podosphaera xanthii, Erysiphe cichoracearum", - type: "fungal", - severity: "moderate", - symptoms: [ - "White powdery fungal growth on upper and lower leaf surfaces", - "Yellowing and browning of leaves starting from older leaves", - "Leaves become brittle, die, and drop prematurely", - "Fruit may be stunted, sunburned, or have poor flavor due to leaf loss", - "Vines may decline prematurely, reducing yield", - ], - causes: [ - "Fungal pathogens specific to cucurbits, favored by warm temperatures and high humidity", - "Spores spread by wind and air currents", - "Dense plantings with poor air circulation", - "Shaded conditions reduce plant vigor and increase susceptibility", - ], - treatment: [ - "Apply sulfur or potassium bicarbonate fungicide at first sign of infection", - "Remove and destroy heavily infected older leaves", - "Improve air circulation through spacing and trellising", - "Apply neem oil or horticultural oil sprays every 7-14 days", - "Use systemic fungicide (myclobutanil) for severe infections", - ], - prevention: [ - "Plant resistant varieties when available", - "Space plants adequately for good air movement", - "Avoid overhead watering; use drip irrigation", - "Apply preventive sulfur spray during favorable weather", - "Remove crop debris at end of season", - ], - }, - { - name: "Downy Mildew (Cucurbits)", - sciName: "Pseudoperonospora cubensis", - type: "fungal", - severity: "high", - symptoms: [ - "Angular yellow to pale green spots on upper leaf surfaces bounded by veins", - "Purple to gray fuzzy growth on corresponding leaf undersides", - "Rapid leaf browning and death (like frost damage)", - "Defoliation can occur within days under favorable conditions", - "Fruit may be sunburned or poorly developed due to leaf loss", - ], - causes: [ - "Oomycete pathogen Pseudoperonospora cubensis", - "Spores blown in from southern regions annually", - "Cool nights (50-65°F) with high humidity and leaf wetness", - "Overhead irrigation and extended dew periods", - ], - treatment: [ - "Apply fungicide containing chlorothalonil, mancozeb, or mefenoxam at first sign", - "Remove and destroy infected leaves", - "Improve air circulation and reduce leaf wetness duration", - "Rotate fungicide chemistries to prevent resistance development", - "Apply systemic fungicide containing azoxystrobin for curative action", - ], - prevention: [ - "Plant resistant varieties when available", - "Avoid overhead irrigation", - "Space plants for good air circulation", - "Monitor local disease alerts for timing of first spray", - "Apply preventive fungicide when conditions favor disease", - ], - }, - { - name: "Angular Leaf Spot (Cucurbits)", - sciName: "Pseudomonas amygdali pv. lachrymans", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Small water-soaked spots on leaves that expand into angular lesions bounded by veins", - "Lesions turn tan or brown and may fall out, leaving ragged holes", - "White crusty bacterial exudate on spots on leaf undersides in dry weather", - "Water-soaked spots on fruit that become white and cracked", - "Defoliation in severe infections", - ], - causes: [ - "Bacterium Pseudomonas amygdali pv. lachrymans", - "Bacteria enter through stomata and wounds", - "Spread by rain splash, overhead irrigation, and contaminated hands", - "Warm wet weather (75-85°F) favors disease", - "Bacteria survive on infected seed and plant debris", - ], - treatment: [ - "Remove and destroy infected leaves and fruit", - "Apply fixed copper bactericide at first sign of disease", - "Avoid overhead irrigation", - "Improve air circulation through spacing and trellising", - "Rotate with non-cucurbit crops for 2 years", - ], - prevention: [ - "Use certified disease-free seed", - "Practice 2-year crop rotation", - "Avoid overhead irrigation", - "Use drip irrigation to keep foliage dry", - "Remove cucurbit volunteer plants", - ], - }, - { - name: "Gummy Stem Blight", - sciName: "Didymella bryoniae (Stagonosporopsis spp.)", - type: "fungal", - severity: "high", - symptoms: [ - "Water-soaked lesions on stems at nodes that enlarge and girdle stems", - "Amber-colored gummy ooze exuding from stem cankers", - "Brown to black circular spots on leaves with concentric rings", - "Fruit rot with dark sunken lesions, especially on watermelon", - "Wilt and death of vines beyond canker point", - ], - causes: [ - "Fungal pathogen surviving in plant debris and on seed", - "Spores splash-dispersed by rain and overhead irrigation", - "Warm wet weather (65-85°F) with high humidity", - "Overwinters in infected crop debris", - ], - treatment: [ - "Remove and destroy infected vines and fruit", - "Apply fungicide containing chlorothalonil or mancozeb", - "Improve air circulation through spacing", - "Avoid overhead irrigation", - "Practice 2-3 year rotation with non-cucurbit crops", - ], - prevention: [ - "Use disease-free seed or treated seed", - "Plant resistant varieties when available", - "Practice crop rotation of 2-3 years", - "Avoid overhead irrigation", - "Remove and destroy crop debris immediately after harvest", - ], - }, - { - name: "Phytophthora Blight (Cucurbits)", - sciName: "Phytophthora capsici", - type: "fungal", - severity: "critical", - symptoms: [ - "Rapid wilting of entire plant despite adequate soil moisture", - "Dark water-soaked lesions on stems at soil line with white fungal growth", - "Water-soaked spots on fruit that expand rapidly with white fuzzy growth", - "Complete plant collapse within days", - "Root and crown rot causing plant death", - ], - causes: [ - "Soil-borne oomycete Phytophthora capsici", - "Spread by contaminated water, soil movement, and infected transplants", - "Warm wet weather with poorly drained soil", - "Spores swim in water and infect through roots and fruit resting on soil", - "Survives in soil for many years as oospores", - ], - treatment: [ - "Remove and destroy infected plants and fruit immediately", - "Improve soil drainage with raised beds", - "Apply fungicides containing mefenoxam or phosphorous acid preventively", - "Do not plant susceptible crops in infested fields for 5 years", - "Use drip irrigation to keep fruit off wet soil", - ], - prevention: [ - "Plant in well-drained soil or raised beds", - "Use drip irrigation, avoid overhead irrigation", - "Mulch to prevent fruit contact with soil", - "Rotate with non-solanaceous, non-cucurbit crops for 5 years", - "Purchase certified disease-free transplants", - ], - }, - ], - }, - - // ── Rosaceae (Rose family) ──────────────────────────────────────────── - { - families: ["Rosaceae"], - templates: [ - { - name: "Fire Blight", - sciName: "Erwinia amylovora", - type: "bacterial", - severity: "critical", - symptoms: [ - "Blossoms suddenly wilt and turn brown or black as if scorched by fire", - "Young shoots wilt and bend over at the tip forming a shepherd's crook shape", - "Brown to black bacterial ooze exuding from cankers in wet weather", - "Cankers on branches with sunken, discolored bark", - "Bacteria spread internally killing entire limbs or trees", - ], - causes: [ - "Bacterium Erwinia amylovora infecting through blossoms and new shoots", - "Spread by pollinating insects, rain splash, and contaminated pruning tools", - "Warm moist weather (75-85°F) during bloom favors infection", - "Excessive nitrogen fertilization promoting succulent growth", - "Fire blight can kill mature trees in a single season", - ], - treatment: [ - "Prune infected branches 12-18 inches below visible cankers during dormant season", - "Sterilize pruning tools with 70% alcohol or 10% bleach between every cut", - "Apply copper-based bactericide or streptomycin during bloom for preventive control", - "Remove and destroy severely infected trees to prevent spread", - "No cure exists for cankers that have reached the trunk or main scaffold limbs", - ], - prevention: [ - "Plant resistant varieties and rootstocks (e.g. 'Liberty', 'Enterprise' apples)", - "Avoid high nitrogen fertilization that promotes succulent growth", - "Prune during dormant season when bacteria are less active", - "Remove fire blight cankers during winter pruning", - "Control sucking insects that can spread bacteria", - ], - }, - { - name: "Apple Scab", - sciName: "Venturia inaequalis", - type: "fungal", - severity: "moderate", - symptoms: [ - "Olive green to dark brown velvety spots on leaves and fruit", - "Leaves may become distorted and drop prematurely", - "Fruit spots become dark, scabby, and cracked, reducing marketability", - "Severe infections cause defoliation by midsummer", - "Fruit set reduced on heavily defoliated trees", - ], - causes: [ - "Fungus Venturia inaequalis specific to apple and crabapple", - "Spores released from infected leaves on ground during spring rains", - "Cool wet weather (55-75°F) during spring green tip through petal fall", - "Extended leaf wetness periods of 9+ hours required for infection", - ], - treatment: [ - "Rake and destroy fallen leaves in fall to reduce spring spore source", - "Apply fungicide sprays from green tip through petal fall every 7-14 days", - "Use protectant fungicides (captan, mancozeb) or systemic (myclobutanil) as needed", - "Improve air circulation through dormant pruning", - "Apply lime sulfur spray at dormant stage for organic control", - ], - prevention: [ - "Plant resistant apple varieties (e.g. 'Liberty', 'Freedom', 'Enterprise')", - "Rake and destroy fallen leaves every autumn", - "Apply preventive fungicide sprays during primary infection period", - "Avoid overhead irrigation that extends leaf wetness", - "Thin canopy through dormant pruning to improve air movement", - ], - }, - { - name: "Cedar-Apple Rust", - sciName: "Gymnosporangium juniperi-virginianae", - type: "fungal", - severity: "moderate", - symptoms: [ - "Bright yellow to orange spots on apple leaves in spring", - "Spots enlarge and develop tiny black dots in center on upper leaf surface", - "Orange cup-like structures develop on leaf undersides in late spring", - "Fruit may develop similar spots that are deformed or drop", - "On cedar: brown, woody galls form that develop orange gelatinous horns in spring rain", - ], - causes: [ - "Rust fungus requiring both apple and red cedar/juniper to complete life cycle", - "Fungus overwinters as galls on juniper branches", - "Spores infect apple leaves during wet spring weather", - "Wind-dispersed spores can travel up to 2 miles", - ], - treatment: [ - "Remove visible cedar galls from nearby juniper trees during winter", - "Apply fungicides (myclobutanil, mancozeb) on apple every 7-14 days from pink through petal fall", - "Remove red cedar/juniper within 1 mile of apple orchard (rarely practical)", - "Plant resistant apple varieties", - "Rake and destroy fallen apple leaves in autumn", - ], - prevention: [ - "Plant resistant apple varieties (e.g. 'Liberty', 'Freedom', 'Red Delicious')", - "Remove cedar galls from nearby junipers before spring", - "Apply fungicide protectant sprays during susceptible period", - "Separate new apple plantings from cedar trees by at least 1 mile", - "Maintain good air circulation through pruning", - ], - }, - { - name: "Brown Rot (Stone Fruit)", - sciName: "Monilinia fructicola", - type: "fungal", - severity: "high", - symptoms: [ - "Blossom blight: flowers turn brown and collapse, often with sticky ooze", - "Fruit rot: small circular brown spots enlarge rapidly covering whole fruit", - "Grayish-brown powdery spore masses on rotting fruit", - "Fruit mummify and remain attached to tree through winter", - "Twig cankers and dieback of small branches", - ], - causes: [ - "Fungus Monilinia fructicola infecting through blossoms and fruit wounds", - "Warm wet weather during bloom and before harvest", - "Insect damage to fruit creates entry points", - "Spores spread by wind, rain, and insects", - "Mummified fruit serve as overwintering source", - ], - treatment: [ - "Remove and destroy mummified fruit from tree and ground after harvest", - "Prune out cankered twigs and branches during winter", - "Apply fungicide at bloom (protectant) and preharvest (systemic)", - "Apply captan, myclobutanil, or propiconazole according to schedule", - "Harvest fruit promptly and handle carefully to avoid bruising", - ], - prevention: [ - "Remove all mummified fruit during dormant season", - "Prune trees annually for good air circulation", - "Thin fruit to reduce clusters and promote drying", - "Control insects that damage fruit", - "Apply preventive fungicide sprays from bloom through preharvest", - ], - }, - { - name: "Black Spot (Rose)", - sciName: "Diplocarpon rosae", - type: "fungal", - severity: "moderate", - symptoms: [ - "Circular black spots with feathery margins on leaves", - "Yellowing of leaf tissue around spots", - "Premature defoliation starting from lower leaves upward", - "Reduced flowering and plant vigor", - "Spots may coalesce causing large blackened areas on leaves", - ], - causes: [ - "Fungus Diplocarpon rosae specific to roses", - "Spores splash from soil or infected leaves during rain and irrigation", - "Warm humid weather with leaf wetness over 7 hours", - "Overcrowding and poor air circulation", - "Infected leaves left on ground from previous season", - ], - treatment: [ - "Remove and destroy all infected leaves and fallen leaf debris", - "Apply fungicide containing chlorothalonil, mancozeb, or myclobutanil every 7-14 days", - "Mulch around roses with 2-3 inches to prevent spore splash", - "Water at soil level early in the day", - "Prune for air circulation and remove diseased canes", - ], - prevention: [ - "Plant black spot resistant rose varieties", - "Water at soil level early in the day", - "Remove and destroy all rose leaves in fall to reduce spring inoculum", - "Space roses for adequate air circulation", - "Apply dormant lime sulfur spray in late winter", - ], - }, - ], - }, - - // ── Brassicaceae (Mustard family) ───────────────────────────────────── - { - families: ["Brassicaceae"], - templates: [ - { - name: "Clubroot", - sciName: "Plasmodiophora brassicae", - type: "fungal", - severity: "high", - symptoms: [ - "Swollen, club-like galls on roots that deform the root system", - "Wilting during hot weather despite adequate soil moisture", - "Stunted growth with yellowing and reddening of leaves", - "Plants fail to thrive and produce small heads or no heads", - "Roots rot at season end, releasing millions of resting spores", - ], - causes: [ - "Soil-borne pathogen Plasmodiophora brassicae specific to brassicas", - "Resting spores survive in soil for up to 20 years", - "Spread by contaminated soil, water, and infected transplants", - "Acidic soil (pH below 6.5) favors disease development", - "Warm moist soil conditions promote infection", - ], - treatment: [ - "Remove and destroy infected plants and as many roots as possible", - "Lime soil to raise pH above 7.0 to reduce disease severity", - "Improve soil drainage to reduce infection", - "No chemical cure available once soil is infested", - "Do not plant susceptible crops for 7+ years in infested soil", - ], - prevention: [ - "Test and lime soil to maintain pH above 6.8", - "Use certified disease-free transplants", - "Practice long crop rotation (5-7 years) with non-brassica crops", - "Avoid moving contaminated soil on tools and equipment", - "Improve soil drainage with raised beds", - ], - }, - { - name: "Black Rot (Brassicas)", - sciName: "Xanthomonas campestris pv. campestris", - type: "bacterial", - severity: "high", - symptoms: [ - "V-shaped yellow lesions starting at leaf margins, pointing toward midvein", - "Blackened veins visible when leaves are held to light", - "Leaves turn brown, dry up, and drop prematurely", - "Yellow to brown discoloration in vascular tissue of stems", - "Heads may be small, discolored, and unmarketable", - ], - causes: [ - "Bacterium Xanthomonas campestris pv. campestris", - "Entering through hydathodes at leaf margins and wounds", - "Spread by contaminated seed, transplants, and irrigation water", - "Warm wet weather (75-85°F) favors rapid spread", - "Bacteria survive in crop debris and cruciferous weeds", - ], - treatment: [ - "Remove and destroy infected plants immediately", - "Rotate with non-brassica crops for 3-5 years", - "Use copper bactericide as preventive spray", - "Avoid overhead irrigation", - "Control cruciferous weeds", - ], - prevention: [ - "Use certified hot-water treated seed or disease-free seed", - "Practice 3-5 year crop rotation with non-brassicas", - "Plant in well-drained soil", - "Avoid overhead irrigation", - "Remove crop debris promptly after harvest", - ], - }, - { - name: "Downy Mildew (Brassicas)", - sciName: "Hyaloperonospora parasitica (formerly Peronospora parasitica)", - type: "fungal", - severity: "moderate", - symptoms: [ - "Yellow to pale green angular spots on upper leaf surfaces", - "White to gray fuzzy growth on leaf undersides beneath spots", - "Spots turn brown and leaves may die", - "Infection may spread to stems and heads", - "Seedlings can be killed by damping off", - ], - causes: [ - "Oomycete pathogen Hyaloperonospora parasitica", - "Favored by cool moist weather (50-65°F) with high humidity", - "Spores spread by wind and water splash", - "Overwinters in crop debris and on volunteer brassicas", - ], - treatment: [ - "Apply fungicide containing chlorothalonil, mancozeb, or mefenoxam", - "Improve air circulation through proper spacing", - "Avoid overhead irrigation", - "Remove and destroy infected plant debris", - "Rotate with non-brassica crops for 2-3 years", - ], - prevention: [ - "Space plants for good air circulation", - "Avoid overhead watering", - "Plant resistant varieties when available", - "Use well-drained soil and avoid crowding", - "Rotate with non-brassica crops", - ], - }, - { - name: "Alternaria Leaf Spot (Brassicas)", - sciName: "Alternaria brassicicola, Alternaria brassicae", - type: "fungal", - severity: "moderate", - symptoms: [ - "Small circular dark spots on leaves with concentric rings and yellow halos", - "Spots enlarge and coalesce, causing leaf blight and defoliation", - "Dark sunken lesions on stems and seed pods", - "Black sooty mold on infected seed pods", - "Seed infection reduces germination and seedling vigor", - ], - causes: [ - "Fungal pathogens Alternaria brassicicola and A. brassicae", - "Spread by infected seed, wind, and rain splash", - "Warm temperatures (65-85°F) with long dew periods", - "Survives on crop debris and cruciferous weeds", - ], - treatment: [ - "Remove and destroy infected leaves", - "Apply copper fungicide or chlorothalonil every 7-14 days", - "Improve air circulation and avoid overhead irrigation", - "Rotate with non-brassica crops for 2 years", - "Use hot-water treated seed (122°F for 25 minutes)", - ], - prevention: [ - "Use disease-free or hot-water treated seed", - "Practice 2-year crop rotation", - "Remove and destroy crop debris", - "Space plants for good air circulation", - "Control cruciferous weeds", - ], - }, - ], - }, - - // ── Fabaceae (Legume family) ────────────────────────────────────────── - { - families: ["Fabaceae"], - templates: [ - { - name: "White Mold (Sclerotinia Rot)", - sciName: "Sclerotinia sclerotiorum", - type: "fungal", - severity: "high", - symptoms: [ - "Water-soaked lesions on stems and branches that become slimy", - "White cottony fungal growth on infected tissue", - "Hard black resting structures (sclerotia) inside stems", - "Sudden wilting and death of branches or entire plants", - "Rot of pods and seeds", - ], - causes: [ - "Fungus Sclerotinia sclerotiorum with very broad host range", - "Hard sclerotia survive in soil for 5+ years", - "Cool moist weather (55-70°F) during flowering favors infection", - "Dense canopy with poor air circulation", - "Spores produced from mushroom-like structures that develop from sclerotia", - ], - treatment: [ - "Remove and destroy infected plants, bagging them to prevent spore spread", - "Improve air circulation through proper spacing", - "Apply fungicide containing boscalid, thiophanate-methyl, or iprodione", - "Deep plow or bury crop debris to bury sclerotia", - "Rotate with non-host crops for 5 years", - ], - prevention: [ - "Use wide row spacing for good air circulation", - "Avoid irrigation during flowering if possible", - "Rotate with grasses and grains for 5+ years", - "Use disease-free seed", - "Bury crop debris with deep tillage", - ], - }, - { - name: "Bacterial Blight (Common/ Halo)", - sciName: "Pseudomonas syringae pv. phaseolicola, Xanthomonas axonopodis pv. phaseoli", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Water-soaked spots on leaves that become brown and necrotic", - "Yellow-green halos surrounding spots (halo blight)", - "Reddish-brown streaks on stems and pods", - "Water-soaked spots on pods that become reddish-brown", - "Seed infection with shriveled or discolored seed", - ], - causes: [ - "Bacterial pathogens specific to beans and other legumes", - "Spread by contaminated seed, rain splash, and irrigation water", - "Warm temperatures (75-90°F) with high humidity", - "Bacteria enter through stomata and wounds", - "Survive in infected seed and crop debris", - ], - treatment: [ - "Remove and destroy severely infected plants", - "Apply fixed copper bactericide at first sign", - "Avoid overhead irrigation", - "Do not work among wet plants", - "Rotate with non-legume crops for 2-3 years", - ], - prevention: [ - "Use certified disease-free seed", - "Plant resistant varieties when available", - "Practice 2-3 year crop rotation", - "Avoid overhead irrigation", - "Remove crop debris promptly after harvest", - ], - }, - { - name: "Bean Rust", - sciName: "Uromyces appendiculatus", - type: "fungal", - severity: "moderate", - symptoms: [ - "Small white spots that develop into reddish-brown powdery pustules on leaf undersides", - "Yellow halos surrounding pustules on upper leaf surfaces", - "Pustules may also appear on stems and pods", - "Leaves turn yellow, dry up, and drop prematurely", - "Severe infections can completely defoliate plants", - ], - causes: [ - "Rust fungus Uromyces appendiculatus specific to beans", - "Spores spread by wind over long distances", - "Free moisture (dew, rain) on leaves required for infection", - "Moderate temperatures (60-80°F) with high humidity", - "Overwinters on infected crop debris and volunteer plants", - ], - treatment: [ - "Remove and destroy infected leaves at first sign", - "Apply sulfur, chlorothalonil, or myclobutanil fungicide every 7-14 days", - "Improve air circulation through trellising", - "Avoid overhead watering", - "Use drip irrigation", - ], - prevention: [ - "Plant resistant varieties", - "Space plants for adequate air circulation", - "Avoid overhead irrigation", - "Remove crop debris at end of season", - "Practice crop rotation with non-legume crops", - ], - }, - { - name: "Charcoal Rot", - sciName: "Macrophomina phaseolina", - type: "fungal", - severity: "high", - symptoms: [ - "Grayish discoloration on stems and roots that becomes dark gray to black", - "Numerous tiny black specks (microsclerotia) on lower stems resembling charcoal dust", - "Sudden wilting and death of plants under heat stress", - "Root and lower stem tissue becomes dry, shredded, and gray", - "Internal stem tissue shows reddish-brown discoloration", - ], - causes: [ - "Fungus Macrophomina phaseolina with over 500 host species", - "Survives in soil and crop debris as microsclerotia for many years", - "High soil temperatures (85-95°F) and drought stress favor disease", - "Enters through roots and colonizes vascular tissue", - "Spread by contaminated soil, infected plant material, and equipment", - ], - treatment: [ - "Remove and destroy infected plants and root systems", - "Irrigate during hot dry weather to reduce heat stress", - "Improve soil organic matter to increase water holding capacity", - "Rotate with non-host crops (grasses) for 3-5 years", - "No effective fungicide treatment once symptoms appear", - ], - prevention: [ - "Maintain adequate soil moisture during hot weather", - "Use irrigation to reduce heat stress", - "Rotate with grain crops for 3-5 years", - "Add organic matter to soil to improve moisture retention", - "Plant tolerant varieties when available", - ], - }, - ], - }, - - // ── Poaceae (Grass family) ──────────────────────────────────────────── - { - families: ["Poaceae"], - templates: [ - { - name: "Stem Rust (Cereals)", - sciName: "Puccinia graminis", - type: "fungal", - severity: "high", - symptoms: [ - "Large reddish-brown oval pustules on stems, leaves, and heads", - "Pustules may merge forming elongated lesions on stems", - "Rust-colored spores rub off easily on hands and clothing", - "Stems weaken and may lodge (fall over) under grain weight", - "Severely infected plants produce shriveled grain", - ], - causes: [ - "Rust fungus Puccinia graminis with multiple formae speciales for specific cereal hosts", - "Spores blown over long distances by wind", - "Free moisture required for spore germination on plant surfaces", - "Moderate temperatures (60-80°F) with high humidity", - "Overwinters in warmer climates on volunteer plants or alternate hosts", - ], - treatment: [ - "Apply fungicide containing azoxystrobin, tebuconazole, or propiconazole at first sign", - "Plant resistant varieties to prevent need for treatment", - "Fungicide timing is critical — apply at flag leaf emergence or first pustules", - "Rotate fungicide chemistries to prevent resistance", - "Destroy volunteer grain and alternate host (barberry) plants", - ], - prevention: [ - "Plant resistant varieties with known Sr resistance genes", - "Eradicate common barberry (alternate host) in areas where it grows", - "Delay planting to avoid peak spore periods", - "Use crop rotation with non-cereal crops", - "Monitor extension disease forecasts", - ], - }, - { - name: "Fusarium Head Blight (Scab)", - sciName: "Fusarium graminearum (Gibberella zeae)", - type: "fungal", - severity: "critical", - symptoms: [ - "Premature bleaching of spikelets on wheat and barley heads", - "Pinkish-orange fungal growth at base of infected spikelets", - "Infected grain shriveled, lightweight, and chalky (tombstone kernels)", - "Reduced yield and test weight", - "Grain contaminated with mycotoxins (DON/deoxynivalenol) toxic to humans and livestock", - ], - causes: [ - "Fungus Fusarium graminearum infecting during flowering", - "Warm wet weather (80-85°F with rain) during anthesis (flowering) period", - "Spores produced on crop residue (corn stalks, wheat straw) on soil surface", - "Spores splash-dispersed upward onto heads during rain", - "No-till farming increases inoculum levels on surface residue", - ], - treatment: [ - "Apply fungicide (tebuconazole, metconazole, prothioconazole) at early flowering (Feekes 10.5.1)", - "Timing is critical — must be applied before infection occurs", - "Harvest early and dry grain below 15% moisture", - "Clean grain to remove lightweight infected kernels", - "Test grain for DON mycotoxin levels before feeding to livestock", - ], - prevention: [ - "Plant moderately resistant varieties", - "Rotate with non-host crops (soybean, alfalfa) for at least 1 year", - "Bury crop residue with tillage to speed decomposition", - "Avoid planting wheat after corn in high-risk areas", - "Monitor Fusarium head blight risk models from extension services", - ], - }, - { - name: "Powdery Mildew (Cereals)", - sciName: "Blumeria graminis f. sp. tritici (wheat), f. sp. hordei (barley)", - type: "fungal", - severity: "moderate", - symptoms: [ - "White to gray powdery fungal growth on leaf blades, sheaths, and heads", - "Yellow chlorotic spots develop under fungal growth", - "Leaves turn brown and die prematurely", - "Reduced tillering, head size, and grain fill", - "Severe infections can cause significant yield loss (10-40%)", - ], - causes: [ - "Formae speciales of Blumeria graminis specific to cereal hosts", - "Overwinters as mycelium on living leaves in mild climates", - "Moderate temperatures (55-75°F) with high humidity", - "Dense canopy with reduced air circulation", - "High nitrogen fertilization increases susceptibility", - ], - treatment: [ - "Apply fungicide containing triazole (tebuconazole, propiconazole) or strobilurin at flag leaf emergence", - "Apply at first sign of infection on lower leaves", - "Rotate fungicide chemistries to prevent resistance", - "Reduce nitrogen rate if disease is severe", - "Plant resistant varieties", - ], - prevention: [ - "Plant resistant varieties with known Pm resistance genes", - "Use balanced nitrogen fertilization", - "Practice crop rotation with non-cereal crops", - "Avoid dense planting that reduces air circulation", - "Scout fields regularly during favorable weather", - ], - }, - ], - }, - - // ── Araceae (Arum family / Houseplants) ────────────────────────────── - { - families: ["Araceae"], - templates: [ - { - name: "Bacterial Leaf Spot (Aroids)", - sciName: "Xanthomonas campestris pv. dieffenbachiae, Pseudomonas spp.", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Water-soaked, angular lesions on leaves that turn yellow then brown", - "Lesions may have a yellow halo surrounding the necrotic center", - "Leaf spots coalesce, causing large blighted areas", - "Soft rot of stems and petioles in advanced stages", - "Foul odor from rotting tissue in severe cases", - ], - causes: [ - "Bacterial pathogens entering through wounds or leaf damage", - "Spread by contaminated pruning tools, splashing water, and handling", - "Warm humid conditions with poor air circulation", - "Overhead watering that keeps leaves wet for extended periods", - "Bacteria survive on infected plant debris and contaminated pots", - ], - treatment: [ - "Remove and destroy infected leaves with sterilized scissors", - "Avoid overhead watering; water at soil level", - "Improve air circulation around plants", - "Apply copper-based bactericide as a foliar spray", - "Isolate infected plants from healthy ones", - ], - prevention: [ - "Use sterile potting mix for all plantings", - "Water at soil level, not on leaves", - "Provide good air circulation through spacing", - "Sterilize pruning tools between plants with alcohol", - "Inspect new plants and quarantine for 2 weeks before introducing", - ], - }, - { - name: "Root Rot (Aroids/Overwatering)", - sciName: "Pythium spp., Phytophthora spp., Rhizoctonia solani", - type: "fungal", - severity: "high", - symptoms: [ - "Yellowing leaves starting from lower leaves, progressing upward", - "Brown, mushy, or slimy roots that disintegrate easily", - "Dark brown to black discoloration of stem base", - "Wilting despite wet soil due to damaged root system", - "Stunted growth with small, pale leaves", - ], - causes: [ - "Soil-borne fungi favored by overwatering and poor drainage", - "Heavy potting soil that retains too much moisture", - "Pots without adequate drainage holes", - "Watering too frequently for the light and temperature conditions", - "Cold temperatures combined with wet soil", - ], - treatment: [ - "Remove plant from pot and trim away all mushy, brown roots", - "Treat remaining roots with fungicide dip or hydrogen peroxide solution", - "Repot in fresh sterile potting mix with added perlite for drainage", - "Reduce watering frequency significantly", - "Place in brighter location with better air circulation", - ], - prevention: [ - "Use well-draining potting mix appropriate for aroids", - "Use containers with drainage holes", - "Water only when top 1-2 inches of soil are dry", - "Avoid letting pots sit in standing water", - "Provide adequate light for the specific plant species", - ], - }, - { - name: "Fungal Leaf Spot (Aroids)", - sciName: "Colletotrichum spp., Cercospora spp., Phyllosticta spp.", - type: "fungal", - severity: "low", - symptoms: [ - "Small circular to irregular spots on leaves that enlarge with time", - "Spots may have tan centers with dark brown or purple borders", - "Yellow halos surrounding individual spots", - "Spots may have small black fruiting bodies visible in the center", - "Leaves become unsightly with reduced photosynthetic area", - ], - causes: [ - "Fungal pathogens common in indoor environments", - "Spread by water splash, contaminated tools, or handling", - "High humidity with poor air circulation", - "Overhead watering that keeps leaves wet", - "Dust accumulation on leaves may promote infection", - ], - treatment: [ - "Remove and destroy infected leaves with sterilized scissors", - "Improve air circulation around plants", - "Reduce leaf wetness by watering at soil level", - "Apply copper fungicide or neem oil spray to remaining leaves", - "Wipe leaves with mild soap solution to reduce surface pathogens", - ], - prevention: [ - "Water at soil level, avoiding leaf wetting", - "Provide good air circulation", - "Wipe leaves periodically to remove dust and potential pathogens", - "Quarantine new plants before introducing them", - "Use sterile potting mix", - ], - }, - ], - }, - - // ── Succulents / Cactaceae / Crassulaceae / Asphodelaceae ───────────── - { - families: ["Cactaceae", "Crassulaceae", "Asphodelaceae"], - templates: [ - { - name: "Stem Rot (Succulents)", - sciName: "Pythium spp., Phytophthora spp., Fusarium spp.", - type: "fungal", - severity: "high", - symptoms: [ - "Soft, mushy, discolored areas at base of stem or on pads", - "Brown to black rot that spreads upward from soil line", - "Leaves turn yellow, translucent, and fall off easily", - "Stem collapses and plant topples over", - "Foul odor from rotting tissue in advanced stages", - ], - causes: [ - "Fungal pathogens entering through wounds or from waterlogged soil", - "Overwatering especially during dormant season", - "Poorly draining potting soil (too organic for succulents)", - "Pots without adequate drainage holes", - "Cold temperatures combined with wet soil", - ], - treatment: [ - "Remove all rotted tissue immediately with sterilized knife — cut well into healthy tissue", - "Allow cutting to callous over for several days before repotting", - "Repot in fresh sterile succulent/cactus mix with excellent drainage", - "Reduce watering frequency to once every 2-4 weeks", - "Apply rooting hormone and fungicide powder to cut surfaces", - ], - prevention: [ - "Use extremely well-draining succulent/cactus potting mix", - "Use containers with drainage holes and avoid oversized pots", - "Water only when soil is completely dry (soak and dry method)", - "Reduce watering dramatically during winter dormant period", - "Provide maximum light possible for the species", - ], - }, - { - name: "Mealybugs (Succulents)", - sciName: "Pseudococcidae family — Planococcus spp., Pseudococcus spp.", - type: "environmental", - severity: "low", - symptoms: [ - "White cottony masses in leaf axils, on stems, and under leaves", - "Sticky honeydew on leaves and surrounding surfaces", - "Sooty mold growing on honeydew", - "Stunted growth and distorted new growth", - "Ants attracted to honeydew may protect mealybugs", - ], - causes: [ - "Sap-feeding insects introduced on new plants or by ants", - "Overfertilization with nitrogen promoting soft growth", - "Overcrowding of plants limiting inspection", - "Warm indoor environments favor year-round reproduction", - ], - treatment: [ - "Remove visible mealybugs with cotton swab dipped in rubbing alcohol", - "Spray with insecticidal soap or neem oil solution, covering all surfaces", - "For severe infestations, use systemic insecticide (imidacloprid) for ornamentals", - "Isolate infested plants from healthy collection", - "Check and treat plants weekly for at least one month", - ], - prevention: [ - "Quarantine and inspect all new plants before adding to collection", - "Inspect plants regularly, especially in leaf axils and under leaves", - "Maintain proper growing conditions to keep plants vigorous", - "Prune out heavily infested plant parts", - "Control ant populations that protect mealybugs", - ], - }, - ], - }, - - // ── Ericaceae (Heath family — blueberries, cranberries, rhododendron) ─ - { - families: ["Ericaceae"], - templates: [ - { - name: "Phytophthora Root Rot (Ericaceous)", - sciName: "Phytophthora cinnamomi, P. cactorum", - type: "fungal", - severity: "high", - symptoms: [ - "Chlorosis (yellowing) of leaves starting from older leaves", - "Leaves turn red or bronze in fall coloration pattern during growing season", - "Stunted growth with reduced leaf and shoot size", - "Root system shows brown decay with no fine feeder roots", - "Sudden wilting and plant death in hot weather", - ], - causes: [ - "Phytophthora species specific to acid-loving plants", - "Poorly drained heavy soils with excess moisture", - "Planting too deeply in heavy clay soils", - "Spread by contaminated irrigation water and nursery stock", - "Fungus survives in soil for many years as oospores", - ], - treatment: [ - "Remove and destroy severely infected plants", - "Improve soil drainage (raised beds, tile drainage)", - "Apply phosphonate fungicide as foliar spray or trunk injection", - "Do not replant ericaceous plants in same location", - "Amend soil with organic matter to improve drainage", - ], - prevention: [ - "Plant in well-drained acidic soil or raised beds", - "Use certified disease-free plants from reputable nurseries", - "Plant at correct depth — not too deep", - "Mulch with acidic organic mulch (pine bark, peat moss)", - "Avoid overwatering and standing water near roots", - ], - }, - { - name: "Mummy Berry (Blueberry)", - sciName: "Monilinia vaccinii-corymbosi", - type: "fungal", - severity: "moderate", - symptoms: [ - "Young leaves, shoots, and flowers turn brown and wilt as if frost-damaged", - "Infected berries turn light pink or cream, then tan, and shrivel into mummies", - "Mummified fruit drops or remains attached to clusters through harvest", - "Fuzzy gray fungal growth on mummies in spring", - "Cup-shaped mushroom-like structures (apothecia) develop from mummies on ground", - ], - causes: [ - "Fungus Monilinia vaccinii-corymbosi infecting through flowers and shoot tips", - "Spores produced by apothecia from overwintered mummies on ground", - "Cool wet weather during bloom", - "Continuous cropping and lack of sanitation", - ], - treatment: [ - "Rake and destroy all mummified fruit from ground and bushes", - "Apply mulch to cover infected mummies on soil surface", - "Apply fungicide (fenbuconazole, propiconazole) at early bloom", - "Cultivate or disk around bushes to bury mummies", - "Remove infected shoots and fruit during season", - ], - prevention: [ - "Rake or cultivate to bury mummies after leaf drop in fall", - "Apply fresh mulch each year to cover remaining mummies", - "Plant resistant varieties", - "Prune bushes for good air circulation and spray penetration", - "Good sanitation is the most effective control", - ], - }, - ], - }, - - // ── Asteraceae (Sunflower family) ───────────────────────────────────── - { - families: ["Asteraceae"], - templates: [ - { - name: "Sclerotinia Wilt (Asteraceae)", - sciName: "Sclerotinia sclerotiorum, Sclerotinia minor", - type: "fungal", - severity: "high", - symptoms: [ - "Sudden wilting of leaves followed by collapse of entire plant", - "Water-soaked lesions on stems that become soft and bleached", - "Cottony white fungal growth on infected tissue", - "Hard black sclerotia (resting structures) inside hollow stems", - "Premature ripening and seed head infection", - ], - causes: [ - "Soil-borne fungus Sclerotinia sclerotiorum with very broad host range", - "Survives in soil as hard black sclerotia for 5+ years", - "Cool moist weather (55-70°F) during flowering", - "Dense plant canopy with poor air circulation", - "Spores produced from mushroom-like apothecia that form from sclerotia", - ], - treatment: [ - "Remove and destroy infected plants immediately — bag to prevent spore spread", - "Improve air circulation through proper spacing and thinning", - "Apply fungicide containing iprodione, boscalid, or thiophanate-methyl", - "Avoid overhead irrigation during flowering period", - "Rotate with grasses and grains for 5-8 years", - ], - prevention: [ - "Use wide row spacing for good air circulation", - "Avoid planting in low areas with poor air drainage", - "Practice long rotation with non-host crops (grasses)", - "Plant resistant varieties when available", - "Bury crop debris with deep tillage", - ], - }, - { - name: "Aster Yellows", - sciName: "Phytoplasma (Candidatus Phytoplasma asteris)", - type: "bacterial", - severity: "moderate", - symptoms: [ - "Yellowing of leaves, often on one side or one part of the plant", - "Abnormal growth — stunting, excessive branching, or witch's broom", - "Flowers become distorted, green, or show phyllody (leaves where petals should be)", - "Chlorotic vein banding and leaf distortion", - "Plants fail to produce normal flowers or seeds", - ], - causes: [ - "Phytoplasma transmitted by leafhoppers (especially aster leafhopper)", - "Phytoplasmas are bacteria without cell walls living in plant phloem", - "Leafhoppers acquire phytoplasma from infected wild plants", - "Weedy areas adjacent to gardens serve as phytoplasma reservoirs", - ], - treatment: [ - "Remove and destroy infected plants to reduce leafhopper infection source", - "Control leafhoppers with insecticide applications or row covers", - "No cure for infected plants", - "Remove weed hosts in and around garden", - "Use reflective mulches to repel leafhoppers", - ], - prevention: [ - "Control leafhoppers with row covers and reflective mulches", - "Remove weeds that serve as pathogen reservoirs", - "Remove symptomatic plants promptly", - "Avoid planting near weedy areas", - "Use insecticide sprays to control leafhopper populations", - ], - }, - ], - }, - - // ── Lamiaceae (Mint family) ─────────────────────────────────────────── - { - families: ["Lamiaceae"], - templates: [ - { - name: "Downy Mildew (Lamiaceae/Basil)", - sciName: "Peronospora belbahrii", - type: "fungal", - severity: "high", - symptoms: [ - "Yellow to pale green angular patches on upper leaf surfaces between veins", - "Dark gray to purplish fuzzy growth on leaf undersides corresponding to yellow patches", - "Leaves turn brown, curl, and drop from plant", - "Defoliation progresses rapidly from lower to upper leaves", - "Plants may be completely defoliated within days to weeks", - ], - causes: [ - "Oomycete pathogen Peronospora belbahrii specific to basil and related Lamiaceae", - "Spores blown in from infested growing regions annually", - "Spores require free moisture and cool nights (60-70°F) to infect", - "Overhead irrigation and dense plantings increase disease severity", - "Pathogen survives in infected plant tissue and on contaminated seed", - ], - treatment: [ - "Remove and destroy all infected plants immediately — do not compost or eat symptomatic leaves", - "Apply fungicide containing copper, mefenoxam, or potassium phosphite preventively", - "Improve air circulation through proper spacing (10-12 inches between plants)", - "Water at soil level, never overhead", - "In severe outbreaks, destroy entire planting and do not replant basil for 2-3 months", - ], - prevention: [ - "Plant resistant basil varieties ('Rustic', 'Prospera', 'Eleonora')", - "Start seed indoors from known clean sources", - "Space plants 10-12 inches apart for good air circulation", - "Water at soil level using drip irrigation", - "Apply copper fungicide preventively when conditions favor disease (cool nights, leaf wetness)", - ], - }, - { - name: "Basil Fusarium Wilt", - sciName: "Fusarium oxysporum f. sp. basilicum", - type: "fungal", - severity: "high", - symptoms: [ - "Sudden wilting of individual stems or entire plants", - "Stunted growth with smaller, yellowing leaves", - "Brown or dark streaks visible in vascular tissue of cut stems", - "Leaves may curl, droop, and drop", - "Uneven growth and plant death as disease progresses", - ], - causes: [ - "Soil-borne fungus Fusarium oxysporum f. sp. basilicum specific to basil", - "Fungus enters through roots and colonizes vascular system", - "Survives in soil for 8-12+ years as resistant chlamydospores", - "Spread by contaminated soil, seed, and infected transplants", - "Warm soil temperatures (75-85°F) favor disease development", - ], - treatment: [ - "Remove and destroy infected plants including as many roots as possible", - "Do not plant basil, mint, or other Lamiaceae in infested soil for 10+ years", - "Solarize soil with clear plastic for 6-8 weeks in summer", - "No chemical cure available once plant is infected", - "Use fresh sterile potting mix for new basil plantings", - ], - prevention: [ - "Use Fusarium-resistant basil varieties ('Nufar', 'Flamingo', 'Amazel')", - "Purchase seed and transplants from reliable sources", - "Use sterile potting mix for containers", - "Practice 10-year rotation with non-Lamiaceae", - "Clean tools and pots thoroughly between plantings", - ], - }, - ], - }, - - // ── Rutaceae (Citrus family) ────────────────────────────────────────── - { - families: ["Rutaceae"], - templates: [ - { - name: "Citrus Canker", - sciName: "Xanthomonas citri subsp. citri", - type: "bacterial", - severity: "high", - symptoms: [ - "Raised, corky, brown lesions with water-soaked margins on leaves, stems, and fruit", - "Lesions are often surrounded by a yellow halo", - "Lesions become brown and scabby with crater-like centers", - "Premature leaf and fruit drop reduces yield", - "Fruit with canker lesions is unmarketable", - ], - causes: [ - "Bacterium Xanthomonas citri subsp. citri entering through stomata and wounds", - "Spread by rain splash, wind-driven rain, and contaminated equipment", - "Spread by leafminer damage creating wound sites", - "Tropical storms and hurricanes can spread bacteria over long distances", - "Warm wet weather favors disease development", - ], - treatment: [ - "Remove and destroy infected leaves, branches, and fruit", - "Apply copper-based bactericide every 14-21 days during susceptible periods", - "Control citrus leafminer to reduce wound sites for infection", - "Prune to improve air circulation and reduce canopy wetness", - "In areas under quarantine, follow regulatory requirements", - ], - prevention: [ - "Plant certified disease-free nursery stock", - "Apply protective copper sprays before predicted rain events", - "Control citrus leafminer with appropriate insecticides", - "Do not move citrus plant material from quarantine areas", - "Maintain windbreaks to reduce spread by wind-driven rain", - ], - }, - { - name: "Huanglongbing (Citrus Greening)", - sciName: "Candidatus Liberibacter asiaticus", - type: "bacterial", - severity: "critical", - symptoms: [ - "Yellowing of leaves in an asymmetric mottled pattern", - "Veins may become yellow or corky on leaves", - "Fruit remains small, misshapen, and green at bottom (color inversion)", - "Fruit has bitter, salty, unpleasant taste and is unmarketable", - "Progressive tree decline and death within 5-10 years", - ], - causes: [ - "Bacterium spread by Asian citrus psyllid (Diaphorina citri)", - "Bacteria colonize phloem tissue, blocking nutrient transport", - "Long incubation period (1-3 years) before symptoms appear", - "No cure exists — infected trees decline and die", - "Disease has devastated citrus in Florida, Brazil, and Asia", - ], - treatment: [ - "Remove infected trees immediately to reduce spread to healthy trees", - "Control Asian citrus psyllid with rigorous insecticide program", - "No cure for infected trees — management focuses on vector control", - "Use systemic insecticides for psyllid control", - "Under quarantine regulation in affected areas", - ], - prevention: [ - "Use certified disease-free nursery stock", - "Maintain rigorous psyllid control program", - "Do not bring citrus plants from quarantine areas", - "Monitor for psyllids with sticky traps", - "Remove and destroy abandoned citrus trees that harbor psyllids", - ], - }, - ], - }, -]; - -// ─── Helper ────────────────────────────────────────────────────────────────── - -export function getTemplatesForFamily(family: string): DiseaseSpec[] { - const result: DiseaseSpec[] = []; - for (const ft of FAMILY_TEMPLATES) { - if (ft.families.includes(family)) { - result.push(...ft.templates); - } - } - return result; -} - -export function slugify(s: string): string { - return s - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .trim() - .replace(/^-|-$/g, ""); -} diff --git a/scripts/expand-diseases.ts b/scripts/expand-diseases.ts deleted file mode 100644 index 43e5317..0000000 --- a/scripts/expand-diseases.ts +++ /dev/null @@ -1,691 +0,0 @@ -/** - * Expand DB with comprehensive plant disease list from Wikipedia. - * - * Reads /tmp/plant_diseases/plant_diseases_comprehensive.txt, - * compares against existing DB entries (by name, case-insensitive), - * and inserts new entries with reasonable defaults. - * - * Usage: - * cd apps/web && export $(grep -v '^#' .env.development | xargs) && npx tsx scripts/expand-diseases.ts - */ - -import "dotenv/config"; -import { readFileSync } from "fs"; -import { eq, sql } from "drizzle-orm"; -import { getDb, closeDb } from "../src/lib/db/index"; -import { plants, diseases } from "../src/lib/db/schema"; -import type { CausalAgentType, Severity } from "../src/lib/types"; - -// ─── Parse the comprehensive list ───────────────────────────────────────────── - -interface DiseaseEntry { - name: string; - sourceUrl: string; -} - -function parseComprehensiveList(filePath: string): DiseaseEntry[] { - const content = readFileSync(filePath, "utf-8"); - const entries: DiseaseEntry[] = []; - const lines = content.split("\n"); - const nameRe = /^\d+\.\s+(.+)$/; - - for (let i = 0; i < lines.length; i++) { - const nameMatch = lines[i].match(nameRe); - if (nameMatch) { - const name = nameMatch[1].trim(); - const urlLine = lines[i + 1]?.trim() || ""; - // Only add if the next line is a valid URL - if (urlLine.startsWith("http")) { - entries.push({ name, sourceUrl: urlLine }); - i++; // skip the URL line - } else { - entries.push({ name, sourceUrl: "" }); - } - } - } - return entries; -} - -// ─── Infer causal agent type from disease name ──────────────────────────────── - -function inferCausalAgent(name: string): CausalAgentType { - const lower = name.toLowerCase(); - - // Bacterial indicators - if ( - lower.startsWith("bacterial ") || - lower.includes(" xanthomonas") || - lower.includes(" pseudomonas") || - lower.includes(" erwinia") || - lower.includes(" ralstonia") || - lower.includes(" clavibacter") || - lower.includes(" streptomyces") || - lower.includes(" agrobacterium") || - lower.includes(" corynebacterium") || - lower.includes(" pectobacterium") || - lower.includes(" dickeya") - ) { - return "bacterial"; - } - - // Viral indicators - strong signals - if ( - lower.includes(" mosaic") || - lower.includes(" yellows") || - lower.includes(" leaf roll") || - lower.includes(" leafroll") || - lower.includes(" ringspot") || - lower.includes(" ring spot") || - lower.includes(" enation") || - lower.includes(" phyllody") || - lower.includes(" witches") || - lower.includes(" witches'") || - lower.includes(" crinkle") || - lower.includes(" rosette") || - lower.includes(" shoestring") || - lower.includes(" tristeza") || - lower.includes(" psorosis") || - lower.includes(" stubborn") || - lower.includes(" greening") || - lower.includes(" vein banding") || - lower.includes(" vein mottle") || - lower.includes(" vein clearing") || - lower.includes(" leaf pucker") || - lower.includes(" pucker leaf") || - lower.includes(" latent") || - lower.includes(" motley") || - lower.includes(" rugose") - ) { - return "viral"; - } - - // Viral - names containing "virus" or "viroid" - if (lower.includes(" virus") || lower.includes(" viroid") || lower.includes(" virosis")) { - return "viral"; - } - - // Nematodes - if ( - lower.includes(" nematode") || - lower.includes(" nematodes") || - lower.includes(" eelworm") || - lower.includes(" root knot") || - lower.includes(" root-knot") || - lower.includes(" cyst ") || - lower.includes(" dagger ") || - lower.includes(" lance ") || - lower.includes(" lesion ") || - lower.includes(" ring ") || - lower.includes(" spiral ") || - lower.includes(" sting ") || - lower.includes(" stubby ") || - lower.includes(" needle ") || - lower.includes(" foliar ") || - lower.includes(" bulb ") || - lower.includes(" reniform ") || - lower.includes(" burrowing ") - ) { - // Check if it's really a nematode name - if (lower.includes("nematode")) return "environmental"; - } - - // Fungal indicators - if ( - lower.includes(" mildew") || - lower.includes(" rust") || - lower.includes(" smut") || - lower.includes(" blight") || - lower.includes(" canker") || - lower.includes(" rot") || - lower.includes(" scab") || - lower.includes(" mold") || - lower.includes(" anthracnose") || - lower.includes(" bunt") || - lower.includes(" ergot") || - lower.includes(" dieback") || - lower.includes(" scald") || - lower.includes(" blotch") || - lower.includes(" speckle") || - lower.includes(" sooty") || - lower.includes(" flyspeck") || - lower.includes(" fusarium") || - lower.includes(" alternaria") || - lower.includes(" botrytis") || - lower.includes(" rhizoctonia") || - lower.includes(" pythium") || - lower.includes(" phytophthora") || - lower.includes(" sclerotinia") || - lower.includes(" verticillium") || - lower.includes(" ascochyta") || - lower.includes(" cercospora") || - lower.includes(" septoria") || - lower.includes(" colletotrichum") || - lower.includes(" phomopsis") || - lower.includes(" diaporthe") || - lower.includes(" diplodia") || - lower.includes(" macrophomina") || - lower.includes(" cylindrocladium") || - lower.includes(" mycosphaerella") || - lower.includes(" helminthosporium") || - lower.includes(" curvularia") || - lower.includes(" bipolaris") || - lower.includes(" exserohilum") || - lower.includes(" dothiorella") || - lower.includes(" fusicoccum") || - lower.includes(" pestalotia") || - lower.includes(" glomerella") || - lower.includes(" nectria") || - lower.includes(" eutypa") || - lower.includes(" armillaria") || - lower.includes(" ganoderma") || - lower.includes(" phoma") || - lower.includes(" cladosporium") || - lower.includes(" penicillium") || - lower.includes(" aspergillus") || - lower.includes(" rhizopus") || - lower.includes(" mucor") || - lower.includes(" downy mildew") || - lower.includes(" powdery mildew") || - lower.includes(" pink rot") || - lower.includes(" pink mold") || - lower.includes(" pink root") || - lower.includes(" gray mold") || - lower.includes(" grey mold") || - lower.includes(" white rot") || - lower.includes(" white mold") || - lower.includes(" brown rot") || - lower.includes(" black rot") || - lower.includes(" soft rot") || - lower.includes(" dry rot") || - lower.includes(" fruit rot") || - lower.includes(" root rot") || - lower.includes(" stem rot") || - lower.includes(" ear rot") || - lower.includes(" crown rot") || - lower.includes(" collar rot") || - lower.includes(" pod rot") || - lower.includes(" kernel rot") || - lower.includes(" stalk rot") || - lower.includes(" head rot") || - lower.includes(" butt rot") || - lower.includes(" stump rot") || - lower.includes(" wood rot") || - lower.includes(" seed rot") || - lower.includes(" leaf spot") || - lower.includes(" leaf blight") || - lower.includes(" leaf blotch") || - lower.includes(" leaf rust") || - lower.includes(" brown spot") || - lower.includes(" black spot") || - lower.includes(" black leg") || - lower.includes(" blackleg") || - lower.includes(" black foot") || - lower.includes(" white rust") || - lower.includes(" white smut") || - lower.includes(" white scab") || - lower.includes(" tar spot") || - lower.includes(" target spot") || - lower.includes(" dollar spot") || - lower.includes(" fairy ring") || - lower.includes(" snow mold") || - lower.includes(" pink disease") || - lower.includes(" thread blight") || - lower.includes(" web blight") || - lower.includes(" sclerotial") || - lower.includes(" sore shin") || - lower.includes(" wart") || - lower.includes(" scurf") || - lower.includes(" silver scurf") || - lower.includes(" shot hole") || - lower.includes(" timber rot") || - lower.includes(" cottony rot") || - lower.includes(" watery rot") || - lower.includes(" sour rot") || - lower.includes(" seepage") || - lower.includes(" bunch rot") || - lower.includes(" noble rot") || - lower.includes(" bitter rot") || - lower.includes(" ripe rot") || - lower.includes(" ring rot") || - lower.includes(" coral spot") || - lower.includes(" stem canker") || - lower.includes(" branch canker") || - lower.includes(" perennial canker") || - lower.includes(" brand canker") || - lower.includes(" blister canker") || - lower.includes(" bleeding canker") || - lower.includes(" bark canker") || - lower.includes(" gum canker") || - lower.includes(" collar crack") || - lower.includes(" fasciation") || - lower.includes(" exobasidium") || - lower.includes(" mycorrhiza") || - lower.includes(" lichen") || - lower.includes(" algal") || - lower.includes(" chlorosis") || - lower.includes(" leaf blister") || - lower.includes(" leaf curl") - ) { - return "fungal"; - } - - // Physiological / environmental indicators - if ( - lower.includes(" sunscald") || - lower.includes(" sunburn") || - lower.includes(" chilling") || - lower.includes(" blossom end rot") || - lower.includes(" edema") || - lower.includes(" deficiency") || - lower.includes(" toxicity") || - lower.includes(" ozone") || - lower.includes(" drought") || - lower.includes(" frost") || - lower.includes(" herbicide") || - lower.includes(" pesticide") || - lower.includes(" phytotoxicity") || - lower.includes(" catface") || - lower.includes(" fruit cracking") || - lower.includes(" russeting") || - lower.includes(" growth crack") || - lower.includes(" mealiness") || - lower.includes(" wind scar") || - lower.includes(" hail") || - lower.includes(" salt ") || - lower.includes(" nutritional") || - lower.includes(" mineral") || - lower.includes(" overwatering") || - lower.includes(" under watering") || - lower.includes(" waterlogging") || - lower.includes(" chemical injury") || - lower.includes(" spray injury") || - lower.includes(" fertilizer burn") || - lower.includes(" lightning") || - lower.includes(" bruising") || - lower.includes(" pressure bruise") || - lower.includes(" impact damage") || - lower.includes(" transit rot") - ) { - return "environmental"; - } - - // Insect/mite/pest indicators - if ( - lower.includes(" mite") || - lower.includes(" beetle") || - lower.includes(" weevil") || - lower.includes(" aphid") || - lower.includes(" bollworm") || - lower.includes(" leaf miner") || - lower.includes(" mealybug") || - lower.includes(" thrips") || - lower.includes(" whitefly") || - lower.includes(" caterpillar") || - lower.includes(" sawfly") || - lower.includes(" scale ") || - lower.includes(" leafhopper") || - lower.includes(" psylla") || - lower.includes(" slug") || - lower.includes(" snail") || - lower.includes(" borer") || - lower.includes(" maggot") || - lower.includes(" grub") || - lower.includes(" earwig") || - lower.includes(" grasshopper") - ) { - return "environmental"; - } - - // Fungal genus names - const fungalGenera = [ - "armillaria", - "aspergillus", - "alternaria", - "botrytis", - "cercospora", - "cladosporium", - "colletotrichum", - "curvularia", - "cylindrocladium", - "diplodia", - "fusarium", - "ganoderma", - "glomerella", - "helminthosporium", - "macrophomina", - "mycosphaerella", - "nectria", - "penicillium", - "pestalotia", - "phoma", - "phomopsis", - "phytophthora", - "pythium", - "rhizoctonia", - "sclerotinia", - "septoria", - "verticillium", - "ascochyta", - "cercoseptoria", - "phaeoisariopsis", - "phaeoseptoria", - "stagonospora", - "stemphylium", - "myrothecium", - "myriogenospora", - "dactuliophora", - "dilophospora", - "coniothecium", - "coniosporium", - "cryptostictis", - "catacauma", - "botryodiplodia", - "botryosphaeria", - "cephalosporium", - "ceratocystis", - "chalara", - "choanephora", - "clitocybe", - "coprinus", - "cordana", - "corticium", - "corynespora", - "coryneum", - "cylindrocarpon", - "cylindrocladiella", - "cylindrosporium", - "cytospora", - "cytosporina", - "dematophora", - "didymella", - "dothiorella", - "drechslera", - "endothia", - "eutypa", - "eutypella", - "exobasidium", - "fusicladium", - "fusicoccum", - "gibberella", - "glomerella", - "gnomonia", - "graphiola", - "guignardia", - "hendersonia", - "hendersonula", - "hymenochaete", - "hypoxylon", - "lasiodiplodia", - "leptosphaeria", - "leucostoma", - "lophodermium", - "macrophoma", - "marasmiellus", - "marasmius", - "massaria", - "monilia", - "monosporascus", - "mystrosporium", - "neocosmospora", - "nigrospora", - "omphalia", - "ophiobolus", - "ovulinia", - "ozonium", - "panagrolaimus", - "periconia", - "pestalosphaeria", - "pestalotiopsis", - "phialophora", - "phymatotrichum", - "physalospora", - "phytophthora", - "plasmodiophora", - "plectosporium", - "polyporus", - "poria", - "pseudocercosporella", - "pseudopeziza", - "pseudoseptoria", - "puccinia", - "pyrenochaeta", - "pythium", - "ramularia", - "rhizoctonia", - "rhizopus", - "rhynchosporium", - "rosellinia", - "sclerophthora", - "sclerotinia", - "sclerotium", - "septoria", - "sphaceloma", - "sphaeropsis", - "spongospora", - "stagonospora", - "stemphylium", - "stereum", - "stigmina", - "thanatephorus", - "thielaviopsis", - "tippula", - "typhula", - "ulocladium", - "uredo", - "ustilago", - "valsa", - "venturia", - "verticillium", - "xylaria", - ]; - for (const genus of fungalGenera) { - if (lower.includes(genus)) return "fungal"; - } - - // Default to fungal (most plant diseases are fungal) - return "fungal"; -} - -// ─── Infer severity ─────────────────────────────────────────────────────────── - -function inferSeverity(name: string): Severity { - const lower = name.toLowerCase(); - if ( - lower.includes(" lethal") || - lower.includes(" devastating") || - lower.includes(" destructive") || - lower.includes(" fatal") || - lower.includes(" severe") || - lower.includes(" blight") || - lower.includes(" wilt") || - lower.includes(" canker") || - lower.includes(" dieback") || - lower.includes(" decline") || - lower.includes(" rot") || - lower.includes(" gall") || - lower.includes(" gummosis") || - lower.includes(" necrosis") || - lower.includes(" erwinia") - ) { - return "high"; - } - if ( - lower.includes(" minor") || - lower.includes(" mild") || - lower.includes(" slight") || - lower.includes(" speckle") || - lower.includes(" fleck") || - lower.includes(" freckle") || - lower.includes(" chlorosis") || - lower.includes(" translucence") || - lower.includes(" superficial") - ) { - return "low"; - } - return "moderate"; -} - -// ─── Generate a deterministic slug ──────────────────────────────────────────── - -function toSlug(name: string): string { - return ( - "wiki-" + - name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, "") - .replace(/-+/g, "-") - ); -} - -// ─── Main ───────────────────────────────────────────────────────────────────── - -async function main() { - const db = getDb(); - - // 1. Get existing disease names from DB - const existingDiseases = await db.select({ name: diseases.name }).from(diseases); - const existingNames = new Set(existingDiseases.map((d) => d.name.toLowerCase().trim())); - - console.log(`Existing diseases in DB: ${existingNames.size}`); - - // 2. Parse the comprehensive list - const entries = parseComprehensiveList("/tmp/plant_diseases/plant_diseases_comprehensive.txt"); - console.log(`Total entries in comprehensive file: ${entries.length}`); - - // 3. Find or create catch-all plants - for (const plantId of ["general", "unknown"]) { - const existing = await db.select().from(plants).where(eq(plants.id, plantId)).get(); - - if (!existing) { - console.log(`Creating '${plantId}' plant for catch-all diseases...`); - await db.insert(plants).values({ - id: plantId, - commonName: plantId === "general" ? "General (Multiple Plants)" : "Unknown Plant", - scientificName: "Various", - family: "Various", - category: "houseplant", - careSummary: - plantId === "general" - ? "General plant diseases affecting multiple species." - : "Plant disease with unknown host plant.", - imageUrl: "", - }); - console.log(`Created '${plantId}' plant.`); - } - } - - // 4. Filter new entries (deduplicate within file + against DB) - const newEntries: DiseaseEntry[] = []; - const skipped: string[] = []; - const seen = new Set(); - - for (const entry of entries) { - const key = entry.name.toLowerCase().trim(); - if (seen.has(key)) continue; - seen.add(key); - - if (existingNames.has(key)) { - skipped.push(entry.name); - } else { - newEntries.push(entry); - } - } - - console.log(`\nNew entries to insert: ${newEntries.length}`); - console.log(`Already existing (skipped): ${skipped.length}`); - - if (skipped.length > 0) { - console.log(`\nFirst 10 skipped (of ${skipped.length}):`); - skipped.slice(0, 10).forEach((s) => console.log(` - ${s}`)); - } - - // 5. Insert new entries in batches - if (newEntries.length === 0) { - console.log("\n✅ No new diseases to insert."); - closeDb(); - return; - } - - const BATCH_SIZE = 50; - let inserted = 0; - let errors = 0; - - for (let i = 0; i < newEntries.length; i += BATCH_SIZE) { - const batch = newEntries.slice(i, i + BATCH_SIZE); - const values = batch.map((entry) => { - const causalAgent = inferCausalAgent(entry.name); - const severity = inferSeverity(entry.name); - return { - id: toSlug(entry.name), - plantId: "general", - name: entry.name, - scientificName: "", - causalAgentType: causalAgent, - description: `A plant disease known as "${entry.name}". Source: Wikipedia.`, - symptoms: [], - causes: [], - treatment: [], - prevention: [], - lookalikeIds: [], - severity, - sourceUrl: entry.sourceUrl, - imageUrl: "", - }; - }); - - try { - await db.insert(diseases).values(values).onConflictDoNothing(); - inserted += values.length; - } catch (err) { - // Fall back to individual inserts for this batch if batch fails - console.log(` Batch failed, trying individually...`); - for (const val of values) { - try { - await db.insert(diseases).values(val).onConflictDoNothing(); - inserted++; - } catch (e2) { - // If it's a duplicate key, count it as skipped - if (String(e2).includes("UNIQUE") || String(e2).includes("duplicate")) { - // Already handled by onConflictDoNothing, shouldn't happen - inserted++; - } else { - console.error(` Error inserting "${val.name}":`, e2); - errors++; - } - } - } - } - - if ((i + BATCH_SIZE) % 200 === 0 || i + BATCH_SIZE >= newEntries.length) { - console.log( - ` Progress: ${Math.min(i + BATCH_SIZE, newEntries.length)}/${newEntries.length} (${inserted} inserted, ${errors} errors)`, - ); - } - } - - // 6. Summary - const totalDiseases = await db - .select({ count: sql`COUNT(*)` }) - .from(diseases) - .get(); - const totalPlants = await db - .select({ count: sql`COUNT(*)` }) - .from(plants) - .get(); - - console.log(`\n📊 Results:`); - console.log(` Inserted: ${inserted}`); - console.log(` Errors: ${errors}`); - console.log(` Skipped (already existed): ${skipped.length}`); - console.log(`\n📊 Database now has:`); - console.log(` ${totalPlants?.count ?? 0} plants`); - console.log(` ${totalDiseases?.count ?? 0} diseases`); - - closeDb(); -} - -main().catch((err) => { - console.error("❌ Failed:", err); - process.exit(1); -}); diff --git a/scripts/fill-brave-images-v2.ts b/scripts/fill-brave-images-v2.ts deleted file mode 100644 index 4526560..0000000 --- a/scripts/fill-brave-images-v2.ts +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env node -/** - * fill-brave-images-v2.ts — Brave Image Search for remaining disease images. - * - * Prioritizes by severity (critical → high → moderate → low). - * Runs at 1 request/sec (Brave free tier rate limit). - * Updates Turso DB directly with found images. - * When current key is exhausted, prompts for next key. - * Falls back to duckduckgo-images-api when all keys are spent. - * - * Usage: - * cd apps/web && npx tsx scripts/fill-brave-images-v2.ts - * - * Pass additional API keys as args: - * npx tsx scripts/fill-brave-images-v2.ts KEY2 KEY3 - */ - -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; - -// Load env -const envPath = resolve(__dirname, "../.env.development"); -try { - const env = readFileSync(envPath, "utf-8"); - for (const line of env.split("\n")) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const eqIdx = trimmed.indexOf("="); - if (eqIdx > 0) { - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (!process.env[key]) process.env[key] = val; - } - } - } -} catch {} - -// Also try .env.local for BRAVE_API_KEY -try { - const envLocal = readFileSync(resolve(__dirname, "../.env.local"), "utf-8"); - for (const line of envLocal.split("\n")) { - const trimmed = line.trim(); - if (trimmed.startsWith("BRAVE_API_KEY=")) { - const val = trimmed.slice("BRAVE_API_KEY=".length).trim(); - if (!process.env.BRAVE_API_KEY) process.env.BRAVE_API_KEY = val; - } - } -} catch {} - -import { getDb, closeDb } from "../src/lib/db/index"; -import { diseases } from "../src/lib/db/schema"; -import { createClient } from "@libsql/client"; -import { sql } from "drizzle-orm"; - -interface DiseaseRow { - id: string; - name: string; - scientificName: string; - severity: string; - plantId: string; -} - -// ─── Config ────────────────────────────────────────────────────────────────── - -const BRAVE_DELAY = 1100; // ms between calls (1 req/sec) -const DB_FLUSH_BATCH = 50; -const MAX_PER_KEY = 1800; // Leave 200 buffer of the 2000/mo limit -const STATE_FILE = resolve(__dirname, ".brave-progress.json"); - -let currentKeyIndex = 0; -let braveKeys: string[] = []; -let callsThisKey = 0; -let totalFound = 0; -// totalSkipped tracking removed — not needed for v2 - -// ─── State persistence ─────────────────────────────────────────────────────── - -interface RunState { - processedIds: string[]; - currentKeyIndex: number; - callsThisKey: number; - totalFound: number; -} - -function loadState(): RunState | null { - try { - return JSON.parse(readFileSync(STATE_FILE, "utf-8")); - } catch { - return null; - } -} - -function saveState(processedIds: string[]) { - writeFileSync( - STATE_FILE, - JSON.stringify( - { - processedIds, - currentKeyIndex, - callsThisKey, - totalFound, - }, - null, - 2, - ), - "utf-8", - ); -} - -// ─── Brave API ─────────────────────────────────────────────────────────────── - -async function braveImageSearch(query: string): Promise { - const key = braveKeys[currentKeyIndex]; - if (!key) return null; - - const url = new URL("https://api.search.brave.com/res/v1/images/search"); - url.searchParams.set("q", query); - url.searchParams.set("count", "3"); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetch(url.toString(), { - headers: { "X-Subscription-Token": key, Accept: "application/json" }, - }); - - if (res.status === 429) { - console.log("\n [RATE LIMITED] Key " + (currentKeyIndex + 1) + " exhausted!"); - return "RATE_LIMITED"; - } - if (!res.ok) return null; - - callsThisKey++; - const data = (await res.json()) as { - results?: Array<{ url: string; thumbnail?: { src?: string } }>; - }; - const results = data?.results ?? []; - if (results.length === 0) return null; - - // Prefer non-stock images - for (const r of results) { - const src = r.thumbnail?.src ?? r.url; - if (src && !/(dreamstime|shutterstock|alamy|istock|123rf)/i.test(src)) { - return src; - } - } - return results[0].thumbnail?.src ?? results[0].url; - } catch { - await new Promise((r) => setTimeout(r, 2000)); - } - } - return null; -} - -// ─── DuckDuckGo fallback ──────────────────────────────────────────────────── - -async function ddgFallbackSearch(query: string): Promise { - try { - // Try to use duckduckgo-images-api if installed - const ddg = await import("duckduckgo-images-api").catch(() => null); - if (ddg) { - const results = await ddg.image_search({ query, moderate: true }); - if (results && results.length > 0) { - for (const r of results) { - if (r.image && !/(dreamstime|shutterstock|alamy|istock|123rf)/i.test(r.image)) { - return r.image; - } - } - return results[0].image || null; - } - } - } catch { - // duckduckgo-images-api not installed - } - return null; -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main() { - console.log("\n🔍 Brave Disease Image Filler v2\n"); - - // Parse keys from args + env - const argsKeys = process.argv.slice(2).filter((a) => !a.startsWith("-")); - const envKey = process.env.BRAVE_API_KEY; - braveKeys = [envKey, ...argsKeys].filter(Boolean) as string[]; - braveKeys = [...new Set(braveKeys)]; // dedup - - if (braveKeys.length === 0) { - console.log("❌ No Brave API keys found."); - console.log(" Set BRAVE_API_KEY in .env.local or pass as argument.\n"); - process.exit(1); - } - console.log(`🔑 ${braveKeys.length} Brave API key(s) available\n`); - - // Load state - const state = loadState(); - if (state) { - currentKeyIndex = state.currentKeyIndex; - callsThisKey = state.callsThisKey; - totalFound = state.totalFound; - console.log( - `📋 Resuming from previous run (${state.processedIds.length} processed, ${totalFound} found)\n`, - ); - } - - // Get diseases from DB - const db = getDb(); - const allDiseases = (await db - .select({ - id: diseases.id, - name: diseases.name, - scientificName: diseases.scientificName, - severity: diseases.severity, - plantId: diseases.plantId, - }) - .from(diseases) - .where(sql`(image_url IS NULL OR image_url = '')`) - .all()) as DiseaseRow[]; - - console.log(`📋 ${allDiseases.length} diseases need images\n`); - - if (allDiseases.length === 0) { - console.log("✅ All diseases already have images!\n"); - closeDb(); - return; - } - - // Sort by severity priority - const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 }; - allDiseases.sort( - (a, b) => - (severityOrder[a.severity as keyof typeof severityOrder] || 99) - - (severityOrder[b.severity as keyof typeof severityOrder] || 99), - ); - - // Filter out already-processed from state - const processedSet = new Set(state?.processedIds || []); - const pending = allDiseases.filter((d) => !processedSet.has(d.id)); - - console.log( - `📊 Prioritization: critical=${allDiseases.filter((d) => d.severity === "critical" && !processedSet.has(d.id)).length}, high=${allDiseases.filter((d) => d.severity === "high" && !processedSet.has(d.id)).length}, moderate=${allDiseases.filter((d) => d.severity === "moderate" && !processedSet.has(d.id)).length}, low=${allDiseases.filter((d) => d.severity === "low" && !processedSet.has(d.id)).length}\n`, - ); - - if (pending.length === 0) { - console.log("✅ All remaining diseases already attempted\n"); - closeDb(); - return; - } - - const raw = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - let updates: Array<{ id: string; url: string }> = []; - const processedIds: string[] = state?.processedIds || []; - let found = totalFound; - let ddgMode = false; - - for (let i = 0; i < pending.length; i++) { - const d = pending[i]; - - // Check if current key needs rotating - if (!ddgMode && callsThisKey >= MAX_PER_KEY) { - if (currentKeyIndex < braveKeys.length - 1) { - currentKeyIndex++; - callsThisKey = 0; - console.log(`\n 🔄 Rotating to key ${currentKeyIndex + 1}/${braveKeys.length}\n`); - } else { - console.log( - `\n ⚠️ All ${braveKeys.length} Brave keys exhausted. Switching to DuckDuckGo fallback.\n`, - ); - ddgMode = true; - // Install duckduckgo-images-api if not available - try { - await import("duckduckgo-images-api"); - } catch { - console.log(" Installing duckduckgo-images-api..."); - const { execSync } = await import("child_process"); - execSync("npm install duckduckgo-images-api", { - cwd: resolve(__dirname, ".."), - stdio: "pipe", - }); - console.log(" Done.\n"); - } - } - } - - // Build search query - const plantName = d.plantId.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); - const query = `${d.name} ${d.scientificName} ${plantName} plant disease`; - const sev = d.severity.padEnd(8); - - process.stdout.write( - ` [${String(i + 1).padStart(4)}/${pending.length}] [${sev}] ${d.name.substring(0, 40).padEnd(42)} `, - ); - - let url: string | null = null; - - if (ddgMode) { - url = await ddgFallbackSearch(query); - if (!url) { - // Try a simpler query - url = await ddgFallbackSearch(`${d.name} disease`); - } - } else { - url = await braveImageSearch(query); - if (url === "RATE_LIMITED") { - // Key exhausted mid-query, try next - if (currentKeyIndex < braveKeys.length - 1) { - currentKeyIndex++; - callsThisKey = 0; - console.log("\n 🔄 Rotating key..."); - url = await braveImageSearch(query); - } else { - console.log("\n ⚠️ All keys exhausted mid-batch!"); - ddgMode = true; - url = await ddgFallbackSearch(query); - } - } - } - - if (url) { - updates.push({ id: d.id, url }); - found++; - processedIds.push(d.id); - console.log("✅"); - } else { - processedIds.push(d.id); // Mark as attempted even if not found - console.log("❌"); - } - - // Flush to DB - if (updates.length >= DB_FLUSH_BATCH) { - await raw.batch( - updates.map((u) => ({ - sql: "UPDATE diseases SET image_url = ?, updated_at = datetime() WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - updates = []; - } - - // Save state every 50 - if ((i + 1) % 50 === 0) { - saveState(processedIds); - } - - // Rate limit (even for DDG to be polite) - await new Promise((r) => setTimeout(r, ddgMode ? 500 : BRAVE_DELAY)); - } - - // Final flush - if (updates.length > 0) { - await raw.batch( - updates.map((u) => ({ - sql: "UPDATE diseases SET image_url = ?, updated_at = datetime() WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - } - - saveState(processedIds); - raw.close(); - - // Final report - const finalList = await db - .select({ id: diseases.id, name: diseases.name, imageUrl: diseases.imageUrl }) - .from(diseases) - .all(); - const w = finalList.filter((d) => d.imageUrl); - const wo = finalList.filter((d) => !d.imageUrl); - - console.log(`\n${"═".repeat(50)}`); - console.log(`📊 BRAVE IMAGE SEARCH COMPLETE`); - console.log(`${"═".repeat(50)}`); - console.log(` Processed: ${pending.length}`); - console.log(` Found this run: ${found - totalFound}`); - console.log(` Total with images: ${w.length}/${finalList.length}`); - console.log(` Still missing: ${wo.length}`); - console.log(` Brave keys used: ${currentKeyIndex + 1}`); - console.log(` Calls on current key: ${callsThisKey}`); - console.log(` DuckDuckGo mode: ${ddgMode}`); - - if (wo.length > 0) { - const rp = resolve(__dirname, ".disease-image-review-needed.md"); - let report = "# Disease Images - Still Missing\n\n"; - report += `Generated: ${new Date().toISOString()}\n\n`; - report += `## Summary\n\n`; - report += `- Total: ${finalList.length}\n`; - report += `- With images: ${w.length}\n`; - report += `- Still missing: ${wo.length}\n\n`; - report += `## Missing Diseases\n\n`; - for (const d of wo) { - report += `- ${d.name} (\`${d.id}\`)\n`; - } - writeFileSync(rp, report, "utf-8"); - console.log(`\n📝 Report: ${rp}`); - } else { - console.log("\n✅ ALL diseases now have images!"); - } - - closeDb(); - console.log("\n"); -} - -main().catch((err) => { - console.error("\n❌", err); - process.exit(1); -}); diff --git a/scripts/fill-brave-images.ts b/scripts/fill-brave-images.ts deleted file mode 100644 index 4a621e1..0000000 --- a/scripts/fill-brave-images.ts +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env node -/** - * fill-brave-images.ts — Brave-only pass for remaining disease images. - * - * Runs at 1 request/sec (Brave rate limit). - * Updates diseases.json and Turso DB. - * - * Usage: cd apps/web && npx tsx scripts/fill-brave-images.ts - */ - -import dotenv from "dotenv"; dotenv.config({ path: resolve(__dirname, "../.env.local") }); -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; -import { createClient } from "@libsql/client"; -import { closeDb } from "../src/lib/db/index"; - -const DISEASES_JSON = resolve(__dirname, "../src/data/diseases.json"); -const BRAVE_KEY = process.env.BRAVE_API_KEY ?? ""; - -interface DiseaseSeed { - id: string; - plantId: string; - name: string; - scientificName: string; - imageUrl?: string; - [key: string]: unknown; -} - -function load(): DiseaseSeed[] { - return JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; -} - -async function searchBraveImage(query: string): Promise { - const url = new URL("https://api.search.brave.com/res/v1/images/search"); - url.searchParams.set("q", query); - url.searchParams.set("count", "3"); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetch(url.toString(), { - headers: { "X-Subscription-Token": BRAVE_KEY, Accept: "application/json" }, - }); - if (res.status === 429) { - await new Promise((r) => setTimeout(r, 5000 * 2 ** attempt)); - continue; - } - if (!res.ok) return null; - const data = (await res.json()) as { - results?: Array<{ url: string; thumbnail?: { src?: string } }>; - }; - const results = data?.results ?? []; - if (results.length === 0) return null; - - // Prefer non-stock direct-looking images - for (const r of results) { - const src = r.thumbnail?.src ?? r.url; - if (src && !/(dreamstime|shutterstock|alamy|istock|123rf)/i.test(src)) return src; - } - return results[0].thumbnail?.src ?? results[0].url; - } catch { - await new Promise((r) => setTimeout(r, 2000)); - } - } - return null; -} - -async function main() { - console.log("\n🔍 Brave Image Search — remaining disease images\n"); - - if (!BRAVE_KEY) { - console.log("❌ No BRAVE_API_KEY in .env.local\n"); - process.exit(1); - } - - const diseases = load(); - const pending = diseases.filter((d) => !d.imageUrl); - console.log(`📋 ${pending.length} diseases need images\n`); - - let found = 0; - - for (let i = 0; i < pending.length; i++) { - const d = pending[i]; - const plant = diseases.find((p) => p.id === d.plantId); - const plantName = plant?.name ?? d.plantId; - const query = `${d.name} ${plantName} plant disease symptom`; - - process.stdout.write(` [${String(i + 1).padStart(2, " ")}/${pending.length}] ${d.name.padEnd(35)} `); - - const url = await searchBraveImage(query); - if (url) { - d.imageUrl = url; - found++; - console.log(`✅`); - } else { - console.log(`❌`); - } - - // 1 req/sec rate limit - await new Promise((r) => setTimeout(r, 1100)); - } - - // Write updated JSON - writeFileSync(DISEASES_JSON, JSON.stringify(diseases, null, 2) + "\n", "utf-8"); - console.log(`\n✅ diseases.json updated: ${found}/${pending.length} images found\n`); - - // Update DB - try { - const dbUrl = process.env.DATABASE_URL; - const dbToken = process.env.DATABASE_TOKEN; - if (dbUrl && dbToken) { - const raw = createClient({ url: dbUrl, authToken: dbToken }); - const updates = pending.filter((d) => d.imageUrl); - for (let i = 0; i < updates.length; i += 50) { - await raw.batch( - updates.slice(i, i + 50).map((d) => ({ - sql: "UPDATE diseases SET image_url = ? WHERE id = ?", - args: [d.imageUrl!, d.id], - })), - "write", - ); - } - raw.close(); - console.log(`✅ Turso DB updated: ${updates.length} rows`); - } else { - console.log("⏭️ Skipping DB — no DATABASE_URL/TOKEN"); - } - } catch (err) { - console.log(` ⚠️ DB: ${err instanceof Error ? err.message : err}`); - } - - // Summary - const finalDiseases = JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; - const stillMissing = finalDiseases.filter((d) => !d.imageUrl); - console.log(`\n${"═".repeat(50)}`); - console.log(`📊 FINAL: ${finalDiseases.length} total`); - console.log(` With images: ${finalDiseases.length - stillMissing.length}`); - console.log(` Still missing: ${stillMissing.length}`); - if (stillMissing.length > 0) { - console.log(`\nStill need human curation:`); - for (const d of stillMissing) { - console.log(` ❌ ${d.name} (${d.id})`); - } - } - console.log(`${"═".repeat(50)}\n`); - - closeDb(); -} - -main().catch((err) => { - console.error("\n❌ Fatal:", err); - process.exit(1); -}); diff --git a/scripts/fill-ddg-images.ts b/scripts/fill-ddg-images.ts deleted file mode 100644 index efafda5..0000000 --- a/scripts/fill-ddg-images.ts +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env node -/** - * fill-ddg-images.ts — DuckDuckGo Image Search for remaining disease images. - * - * No API key needed. Searches DuckDuckGo Images API for each disease - * without an image and updates the Turso DB. - * - * Prioritizes by severity (critical → high → moderate → low). - * Runs at 1 request/sec to be polite to DuckDuckGo. - * Resumable via state file (scripts/.ddg-progress.json). - * - * Usage: - * cd apps/web && npx tsx scripts/fill-ddg-images.ts - */ - -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; - -// Load .env.development for DB creds -const envPath = resolve(__dirname, "../.env.development"); -try { - const env = readFileSync(envPath, "utf-8"); - for (const line of env.split("\n")) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const eqIdx = trimmed.indexOf("="); - if (eqIdx > 0) { - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (!process.env[key]) process.env[key] = val; - } - } - } -} catch {} - -import { getDb, closeDb } from "../src/lib/db/index"; -import { diseases } from "../src/lib/db/schema"; -import { createClient } from "@libsql/client"; -import { sql } from "drizzle-orm"; - -// DuckDuckGo -import { imageSearch } from "@mudbill/duckduckgo-images-api"; - -interface DiseaseRow { - id: string; - name: string; - scientificName: string; - severity: string; - plantId: string; -} - -// ─── Config ────────────────────────────────────────────────────────────────── - -const POLITE_DELAY = 800; // ms between calls -const DB_FLUSH_BATCH = 50; -const STATE_FILE = resolve(__dirname, ".ddg-progress.json"); - -interface RunState { - processedIds: string[]; - totalFound: number; -} - -function loadState(): RunState | null { - try { - return JSON.parse(readFileSync(STATE_FILE, "utf-8")); - } catch { - return null; - } -} - -function saveState(processedIds: string[], totalFound: number) { - writeFileSync(STATE_FILE, JSON.stringify({ processedIds, totalFound }, null, 2), "utf-8"); -} - -// ─── DuckDuckGo Search ─────────────────────────────────────────────────────── - -async function searchImage(query: string): Promise { - try { - const results = await imageSearch({ query, safe: true, iterations: 1, retries: 2 }); - if (!results || results.length === 0) return null; - - // Prefer non-stock images - for (const r of results) { - if (r.image && !/(dreamstime|shutterstock|alamy|istock|123rf)/i.test(r.image)) { - return r.image; - } - } - return results[0].image || results[0].thumbnail || null; - } catch { - // DuckDuckGo may block or timeout; silently skip - return null; - } -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main() { - console.log("\n🦆 DuckDuckGo Disease Image Filler\n"); - - const db = getDb(); - - // Load state - const state = loadState(); - const processedSet = new Set(state?.processedIds || []); - const totalFoundPrev = state?.totalFound ?? 0; - - // Get all diseases that still need images - const allDiseases = (await db - .select({ - id: diseases.id, - name: diseases.name, - scientificName: diseases.scientificName, - severity: diseases.severity, - plantId: diseases.plantId, - }) - .from(diseases) - .where(sql`(image_url IS NULL OR image_url = '')`) - .all()) as DiseaseRow[]; - - console.log(`📋 ${allDiseases.length} diseases need images\n`); - - if (allDiseases.length === 0) { - console.log("✅ All diseases already have images!\n"); - closeDb(); - return; - } - - // Sort by severity: critical > high > moderate > low - const severityOrder: Record = { critical: 0, high: 1, moderate: 2, low: 3 }; - allDiseases.sort((a, b) => (severityOrder[a.severity] ?? 99) - (severityOrder[b.severity] ?? 99)); - - // Filter out already-processed - const pending = allDiseases.filter((d) => !processedSet.has(d.id)); - - console.log( - `📊 Remaining: critical=${allDiseases.filter((d) => d.severity === "critical" && !processedSet.has(d.id)).length}, ` + - `high=${allDiseases.filter((d) => d.severity === "high" && !processedSet.has(d.id)).length}, ` + - `moderate=${allDiseases.filter((d) => d.severity === "moderate" && !processedSet.has(d.id)).length}, ` + - `low=${allDiseases.filter((d) => d.severity === "low" && !processedSet.has(d.id)).length}\n`, - ); - - if (pending.length === 0) { - console.log("✅ All remaining diseases already attempted\n"); - closeDb(); - return; - } - - const raw = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - const processedIds: string[] = state?.processedIds ?? []; - let found = totalFoundPrev; - let updates: Array<{ id: string; url: string }> = []; - - for (let i = 0; i < pending.length; i++) { - const d = pending[i]; - const sev = d.severity.padEnd(8); - - // Build search query — "[disease] on [plant]" phrasing for better specificity - const plantName = d.plantId.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); - const query1 = `${d.name} on ${plantName} plant disease`; - const query2 = `${d.scientificName || d.name} on ${plantName} disease`; - const query3 = `${d.name} plant disease ${plantName}`; - const query4 = `${d.name} plant`; - const query5 = `${d.name} symptom`; - - process.stdout.write( - ` [${String(i + 1).padStart(4)}/${pending.length}] [${sev}] ${d.name.substring(0, 42).padEnd(44)} `, - ); - - // Try queries in order until we get a result - let url: string | null = null; - for (const q of [query1, query2, query3, query4, query5]) { - url = await searchImage(q); - if (url) break; - } - - if (url) { - updates.push({ id: d.id, url }); - found++; - processedIds.push(d.id); - console.log("✅"); - } else { - processedIds.push(d.id); - console.log("❌"); - } - - // Flush to DB in batches - if (updates.length >= DB_FLUSH_BATCH) { - await raw.batch( - updates.map((u) => ({ - sql: "UPDATE diseases SET image_url = ?, updated_at = datetime() WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - updates = []; - } - - // Save state every 50 - if ((i + 1) % 50 === 0) { - saveState(processedIds, found); - } - - // Be polite — 1 req/sec - await new Promise((r) => setTimeout(r, POLITE_DELAY)); - } - - // Final flush - if (updates.length > 0) { - await raw.batch( - updates.map((u) => ({ - sql: "UPDATE diseases SET image_url = ?, updated_at = datetime() WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - } - - saveState(processedIds, found); - raw.close(); - - // Final report - const finalList = await db - .select({ id: diseases.id, name: diseases.name, imageUrl: diseases.imageUrl }) - .from(diseases) - .all(); - const w = finalList.filter((d) => d.imageUrl); - const wo = finalList.filter((d) => !d.imageUrl); - - console.log(`\n${"═".repeat(50)}`); - console.log(`🦆 DUCKDUCKGO SEARCH COMPLETE`); - console.log(`${"═".repeat(50)}`); - console.log(` Processed: ${pending.length}`); - console.log(` Found this run: ${found - totalFoundPrev}`); - console.log(` Total with images: ${w.length}/${finalList.length}`); - console.log(` Still missing: ${wo.length}`); - - if (wo.length > 0) { - const reportPath = resolve(__dirname, ".ddg-image-review-needed.md"); - let report = "# Disease Images - Still Missing (DDG)\n\n"; - report += `Generated: ${new Date().toISOString()}\n\n`; - report += `## Summary\n\n`; - report += `- Total: ${finalList.length}\n`; - report += `- With images: ${w.length}\n`; - report += `- Still missing: ${wo.length}\n\n`; - report += `## Missing Diseases\n\n`; - for (const d of wo) { - report += `- ${d.name} (\`${d.id}\`)\n`; - } - writeFileSync(reportPath, report, "utf-8"); - console.log(`\n📝 Missing report: ${reportPath}`); - } else { - console.log("\n✅ ALL diseases now have images!"); - } - - closeDb(); - console.log(); -} - -main().catch((err) => { - console.error("\n❌ Fatal:", err); - process.exit(1); -}); diff --git a/scripts/fill-disease-images.ts b/scripts/fill-disease-images.ts deleted file mode 100644 index 873cbe1..0000000 --- a/scripts/fill-disease-images.ts +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env node -/** - * fill-disease-images.ts — Three-stage disease image pipeline - * - * For every disease without an imageUrl, tries: - * Stage 1 — Wikipedia search → pageimages - * Stage 2 — Wikimedia Commons search - * Stage 3 — Brave Image Search API (fallback, 1 req/sec, 2000/mo) - * - * Updates both diseases.json (seed) and the Turso DB. - * Flags anything found only via Brave for human review. - * - * Usage: cd apps/web && npx tsx scripts/fill-disease-images.ts - */ - -import "dotenv/config"; -import { readFileSync, writeFileSync, existsSync } from "fs"; -import { resolve } from "path"; -import { createClient } from "@libsql/client"; -import { closeDb } from "../src/lib/db/index"; - -// ─── Types & Config ────────────────────────────────────────────────────────── - -interface DiseaseSeed { - id: string; - plantId: string; - name: string; - scientificName: string; - commonName?: string; - [key: string]: unknown; -} - -interface ImageResult { - url: string; - source: "wikipedia" | "commons" | "brave" | "missing"; - quality: "good" | "fallback" | "missing"; -} - -const DISEASES_JSON = resolve(__dirname, "../src/data/diseases.json"); -const RESULTS_FILE = resolve(__dirname, ".image-results.json"); -const REPORT_FILE = resolve(__dirname, ".image-review-needed.md"); - -const WIKI_API = "https://en.wikipedia.org/w/api.php"; -const COMMONS_API = "https://commons.wikimedia.org/w/api.php"; -const BRAVE_KEY = process.env.BRAVE_API_KEY ?? ""; -const BRAVE_DELAY = 1100; -const MAX_BRAVE = 2000; -const UA = "PlantHealthKB/1.0 (plant-disease-id)"; -const ORIGIN = "*"; - -let braveCount = 0; - -// ─── Wikipedia Stage ───────────────────────────────────────────────────────── - -/** - * Search Wikipedia and get thumbnails in ONE API call using generator=search. - * Returns first thumbnail found, or null. - */ -async function wikiSearchAndThumb(query: string): Promise { - const params = new URLSearchParams({ - action: "query", - generator: "search", - gsrsearch: query, - gsrlimit: "5", - prop: "pageimages", - pithumbsize: "600", - format: "json", - origin: ORIGIN, - }); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetchWithTimeout(`${WIKI_API}?${params}`, { - headers: { "User-Agent": UA }, - }); - if (res.status === 429) { - await delay(3000 * 2 ** attempt); - continue; - } - if (!res.ok) return null; - const data = (await res.json()) as { - query?: { pages?: Record }; - }; - const pages = data?.query?.pages; - if (!pages) return null; - for (const [, p] of Object.entries(pages)) { - const src = (p as { thumbnail?: { source: string } })?.thumbnail?.source; - if (src) return src; - } - return null; - } catch { - await delay(2000); - } - } - return null; -} - -/** - * Try to find a Wikipedia image for a disease. - * Uses generator=search which combines search + thumbnails in one call. - */ -async function wikiStage(d: DiseaseSeed, plantName: string): Promise { - // Try 1: disease name + plant name (most specific) - return wikiSearchAndThumb(`"${d.name}" ${plantName}`); -} - -// ─── Commons Stage ─────────────────────────────────────────────────────────── - -/** Fetch with timeout. Aborts after `ms` milliseconds. */ -async function fetchWithTimeout(url: string, opts: RequestInit, ms = 15000): Promise { - const ctrl = new AbortController(); - const timer = setTimeout(() => ctrl.abort(), ms); - try { - const res = await fetch(url, { ...opts, signal: ctrl.signal }); - return res; - } finally { - clearTimeout(timer); - } -} - -async function commonsSearchAndThumb(query: string): Promise { - const params = new URLSearchParams({ - action: "query", - list: "search", - srsearch: query, - srnamespace: "6", - srlimit: "5", - format: "json", - origin: ORIGIN, - }); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetchWithTimeout(`${COMMONS_API}?${params}`, { - headers: { "User-Agent": UA }, - }); - if (res.status === 429) { - await delay(3000 * 2 ** attempt); - continue; - } - if (!res.ok) return null; - const data = (await res.json()) as { - query?: { search?: Array<{ pageid: number; title: string }> }; - }; - const hits = data?.query?.search ?? []; - if (hits.length === 0) return null; - - // Batch-fetch imageinfo for all found page IDs - const pageids = hits.map((h) => h.pageid).join("|"); - const imgParams = new URLSearchParams({ - action: "query", - pageids, - prop: "imageinfo", - iiprop: "url", - iiurlwidth: "600", - format: "json", - origin: ORIGIN, - }); - - const imgRes = await fetchWithTimeout(`${COMMONS_API}?${imgParams}`, { - headers: { "User-Agent": UA }, - }); - if (!imgRes.ok) return null; - const imgData = (await imgRes.json()) as { - query?: { pages?: Record }; - }; - const imgPages = imgData?.query?.pages; - if (!imgPages) return null; - - for (const [, pg] of Object.entries(imgPages)) { - const p = pg as Record; - const info = (p.imageinfo as Array> | undefined)?.[0]; - if (info?.thumburl) return info.thumburl as string; - if (info?.url) return info.url as string; - } - return null; - } catch { - await delay(2000); - } - } - return null; -} - -async function commonsStage(d: DiseaseSeed, plantName: string): Promise { - let q: string; - if (d.scientificName && !d.scientificName.includes("spp.") && !d.scientificName.includes("/")) { - q = `${d.scientificName} ${plantName}`; - } else { - q = `${d.name} ${plantName} disease`; - } - - const url = await commonsSearchAndThumb(q); - return url ?? null; -} - -// ─── Brave Stage ───────────────────────────────────────────────────────────── - -async function braveStage(d: DiseaseSeed, plantName: string): Promise { - if (!BRAVE_KEY || braveCount >= MAX_BRAVE) return null; - - const url = new URL("https://api.search.brave.com/res/v1/images/search"); - url.searchParams.set("q", `${d.name} ${plantName} plant disease symptom`); - url.searchParams.set("count", "5"); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetchWithTimeout(url.toString(), { - headers: { "X-Subscription-Token": BRAVE_KEY, Accept: "application/json" }, - }); - if (res.status === 429) { - await delay(5000 * 2 ** attempt); - continue; - } - if (!res.ok) return null; - braveCount++; - const data = (await res.json()) as { - results?: Array<{ url: string; thumbnail?: { src?: string } }>; - }; - const results = data?.results ?? []; - if (results.length === 0) return null; - - // Prefer non-stock thumbnails - for (const r of results) { - const src = r.thumbnail?.src ?? r.url; - if (src && !src.includes("dreamstime") && !src.includes("shutterstock") && - !src.includes("alamy") && !src.includes("istock") && !src.includes("123rf")) { - return src; - } - } - return results[0].thumbnail?.src ?? results[0].url; - } catch { - await delay(2000); - } - } - return null; -} - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function delay(ms: number): Promise { - return new Promise((r) => setTimeout(r, ms)); -} - -function loadDiseases(): DiseaseSeed[] { - return JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; -} - -function getPlantName(diseases: DiseaseSeed[], diseaseId: string): string { - const plant = diseases.find((p) => p.id === diseaseId); - return plant?.commonName ?? plant?.name ?? diseaseId; -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main() { - console.log("\n🔍 Plant Disease Image Filler\n"); - - const diseases = loadDiseases(); - console.log(`📋 ${diseases.length} diseases loaded\n`); - - // Load existing results - let results: Record = {}; - if (existsSync(RESULTS_FILE)) { - try { results = JSON.parse(readFileSync(RESULTS_FILE, "utf-8")); } catch { /* fresh */ } - } - - const pending = diseases.filter((d) => { - if ((d.imageUrl as string)?.length) return false; - return !results[d.id]; - }); - - if (pending.length === 0) { - console.log("✅ All done\n"); - await applyResults(diseases, results); - return; - } - - console.log(`⏳ ${pending.length} need images\n`); - - // ── Stage 1: Wikipedia ────────────────────────────────────────────── - const s1 = pending.filter((d) => !results[d.id]); - let s1ok = 0; - console.log("─── Wikipedia ───\n"); - - for (let i = 0; i < s1.length; i++) { - const d = s1[i]; - const plantName = getPlantName(diseases, d.plantId); - const url = await wikiStage(d, plantName); - if (url) { - results[d.id] = { url, source: "wikipedia", quality: "good" }; - s1ok++; - } - const pct = ((i + 1) / s1.length * 100).toFixed(0); - process.stdout.write(` [${pct}% ${i + 1}/${s1.length}] ${d.name.substring(0, 40).padEnd(42)} ${url ? "✅" : "⏭️"}\n`); - if ((i + 1) % 25 === 0) writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2)); - } - - writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2)); - console.log(`\n → ${s1ok}/${s1.length} found\n`); - - // ── Stage 2: Commons ───────────────────────────────────────────────── - const s2 = pending.filter((d) => !results[d.id]); - let s2ok = 0; - - if (s2.length > 0) { - console.log("─── Wikimedia Commons ───\n"); - for (let i = 0; i < s2.length; i++) { - const d = s2[i]; - const plantName = getPlantName(diseases, d.plantId); - let url: string | null = null; - try { - const result = await Promise.race([ - commonsStage(d, plantName), - new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 25000)), - ]); - url = result; - } catch { /* timeout */ } - if (url) { - results[d.id] = { url, source: "commons", quality: "good" }; - s2ok++; - } - const pct = ((i + 1) / s2.length * 100).toFixed(0); - process.stdout.write(` [${pct}% ${i + 1}/${s2.length}] ${d.name.substring(0, 40).padEnd(42)} ${url ? "✅" : "⏭️"}\n`); - - if ((i + 1) % 10 === 0) writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2)); - } - writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2)); - console.log(`\n → ${s2ok}/${s2.length} found\n`); - } - - // ── Stage 3: Brave ─────────────────────────────────────────────────── - const s3 = pending.filter((d) => !results[d.id]); - let s3ok = 0; - - if (s3.length > 0 && BRAVE_KEY) { - console.log("─── Brave Image Search ───\n"); - for (const d of s3) { - if (braveCount >= MAX_BRAVE) { - results[d.id] = { url: "", source: "missing", quality: "missing" }; - continue; - } - const plantName = getPlantName(diseases, d.plantId); - const url = await braveStage(d, plantName); - if (url) { - results[d.id] = { url, source: "brave", quality: "fallback" }; - s3ok++; - process.stdout.write(` ✅ ${d.name}\n`); - } else { - results[d.id] = { url: "", source: "missing", quality: "missing" }; - process.stdout.write(` ❌ ${d.name}\n`); - } - await delay(BRAVE_DELAY); - } - writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2)); - console.log(`\n → ${s3ok}/${s3.length} found via Brave\n`); - } else if (s3.length > 0) { - console.log("─── Brave Image Search ─── → skipped (no key)\n"); - for (const d of s3) results[d.id] = { url: "", source: "missing", quality: "missing" }; - } - - // ── Apply ─────────────────────────────────────────────────────────── - await applyResults(diseases, results); - - // ── Report ────────────────────────────────────────────────────────── - const good = Object.values(results).filter((r) => r.quality === "good").length; - const fallback = Object.values(results).filter((r) => r.quality === "fallback").length; - const missing = Object.values(results).filter((r) => r.quality === "missing").length; - - let report = `# Disease Images — Human Review Needed\n\n`; - report += `Generated: ${new Date().toISOString()}\n\n`; - - for (const [label, ids, type] of [ - ["Fallback (Brave)", Object.entries(results).filter(([, r]) => r.quality === "fallback").map(([id]) => id), "fallback"], - ["Missing", Object.entries(results).filter(([, r]) => r.quality === "missing").map(([id]) => id), "missing"], - ] as const) { - if (ids.length === 0) continue; - report += `## ${type === "fallback" ? "⚠️" : "🚫"} ${label}\n\n`; - for (const id of ids) { - const d = diseases.find((x) => x.id === id); - const r = results[id]; - report += `- **${d?.name ?? id}** (${d?.scientificName ?? ""}) on \`${d?.plantId ?? ""}\``; - if (r?.url) report += `\n ${r.url}`; - report += `\n\n`; - } - } - - if (good === diseases.length) report += `## ✅ All images found!\n`; - writeFileSync(REPORT_FILE, report, "utf-8"); - console.log(`📝 Review report: ${REPORT_FILE}`); - - console.log(`\n${"═".repeat(50)}`); - console.log(`📊 Total: ${diseases.length} Good: ${good} Fallback: ${fallback} Missing: ${missing}`); - console.log(` Brave calls: ${braveCount}`); - console.log(`${"═".repeat(50)}\n`); - - closeDb(); -} - -// ─── Apply results to JSON + DB ────────────────────────────────────────────── - -async function applyResults(diseases: DiseaseSeed[], results: Record) { - const urlMap = new Map( - Object.entries(results).filter(([id, r]) => r.url.length > 0 && diseases.some((d) => d.id === id)), - ); - if (urlMap.size === 0) return console.log("⏭️ No images to apply"); - - // JSON - let n = 0; - const updated = diseases.map((d) => { - const img = urlMap.get(d.id); - if (img) { n++; return { ...d, imageUrl: img.url, imageQuality: img.quality }; } - return d; - }); - writeFileSync(DISEASES_JSON, JSON.stringify(updated, null, 2) + "\n"); - console.log(`✅ diseases.json: ${n} images`); - - // DB - try { - const dbUrl = process.env.DATABASE_URL; - const dbToken = process.env.DATABASE_TOKEN; - if (!dbUrl || !dbToken) return console.log(" ⏭️ DB: no DATABASE_URL/TOKEN"); - const raw = createClient({ url: dbUrl, authToken: dbToken }); - const entries = Array.from(urlMap.entries()); - for (let i = 0; i < entries.length; i += 50) { - await raw.batch( - entries.slice(i, i + 50).map(([id, img]) => ({ - sql: "UPDATE diseases SET image_url = ? WHERE id = ?", - args: [img.url, id], - })), - "write", - ); - } - raw.close(); - console.log(`✅ Turso DB: ${entries.length} rows`); - } catch (err) { - console.log(` ⚠️ DB: ${err instanceof Error ? err.message : err}`); - } -} - -main().catch((err) => { console.error("\n❌", err); process.exit(1); }); diff --git a/scripts/fill-plant-images-v2.ts b/scripts/fill-plant-images-v2.ts deleted file mode 100644 index 9c84b21..0000000 --- a/scripts/fill-plant-images-v2.ts +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/env node -/** - * fill-plant-images-v2.ts — Batch Wikipedia image fetch for remaining plants. - * - * Phase 1: Query 50 scientific names at a time via pageimages. - * Phase 2: Query 50 common names at a time. - * Phase 3: Search individually for stragglers. - * - * Usage: cd apps/web && npx tsx scripts/fill-plant-images-v2.ts - */ - -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; - -// Load env -const envPath = resolve(__dirname, "../.env.development"); -try { - const env = readFileSync(envPath, "utf-8"); - for (const line of env.split("\n")) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const eqIdx = trimmed.indexOf("="); - if (eqIdx > 0) { - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (!process.env[key]) { - process.env[key] = val; - } - } - } - } -} catch (e) {} - -import { getDb, closeDb } from "../src/lib/db/index"; -import { plants } from "../src/lib/db/schema"; -import { createClient } from "@libsql/client"; -import { sql } from "drizzle-orm"; - -const API = "https://en.wikipedia.org/w/api.php"; -const UA = "PlantHealthKB/1.0"; -const BATCH = 50; - -interface PlantRow { - id: string; - commonName: string; - scientificName: string; -} - -function clean(s: string): string { - return s - .replace(/[xX]/g, "x") - .replace(/\s*spp\.?\s*/gi, "") - .replace(/[.\u00d7']/g, "") - .trim(); -} - -async function fetchThumbs(titles: string[]): Promise> { - if (titles.length === 0) { - return new Map(); - } - const p = new URLSearchParams({ - action: "query", - titles: titles.join("|"), - prop: "pageimages", - pithumbsize: "400", - redirects: "1", - format: "json", - }); - for (let a = 0; a < 3; a++) { - try { - const r = await fetch(API + "?" + p.toString(), { - headers: { "User-Agent": UA }, - }); - if (r.status === 429) { - await new Promise((rr) => setTimeout(rr, 5000 * Math.pow(2, a))); - continue; - } - if (!r.ok) { - return new Map(); - } - const d = (await r.json()) as any; - const pages = d?.query?.pages; - if (!pages) { - return new Map(); - } - const m = new Map(); - for (const [, pg] of Object.entries(pages)) { - const p2 = pg as any; - if (!p2.missing && p2.thumbnail?.source) { - m.set(p2.title.toLowerCase(), p2.thumbnail.source); - } - } - return m; - } catch (e) { - await new Promise((rr) => setTimeout(rr, 2000)); - } - } - return new Map(); -} - -async function searchOne(query: string): Promise { - const p = new URLSearchParams({ - action: "query", - generator: "search", - gsrsearch: query, - gsrlimit: "3", - prop: "pageimages", - pithumbsize: "400", - format: "json", - }); - for (let a = 0; a < 3; a++) { - try { - const r = await fetch(API + "?" + p.toString(), { - headers: { "User-Agent": UA }, - }); - if (r.status === 429) { - await new Promise((rr) => setTimeout(rr, 5000 * Math.pow(2, a))); - continue; - } - if (!r.ok) { - return null; - } - const d = (await r.json()) as any; - const pages = d?.query?.pages; - if (!pages) { - return null; - } - for (const [, pg] of Object.entries(pages)) { - const p2 = pg as any; - if (p2.thumbnail?.source) { - return p2.thumbnail.source; - } - } - return null; - } catch (e) { - await new Promise((rr) => setTimeout(rr, 2000)); - } - } - return null; -} - -async function batchPhase( - plants: PlantRow[], - titleFn: (p: PlantRow) => string, - label: string, - dbClient: any, -): Promise { - const remaining: PlantRow[] = []; - const updates: Array<{ id: string; url: string }> = []; - - for (let i = 0; i < plants.length; i += BATCH) { - const chunk = plants.slice(i, i + BATCH); - const titles = chunk.map(titleFn).filter((t) => t.length > 2); - console.log( - " [" + - label + - "] " + - (i + 1) + - "-" + - Math.min(i + BATCH, plants.length) + - "/" + - plants.length + - " ", - ); - const imageMap = await fetchThumbs(titles); - let n = 0; - for (const pl of chunk) { - const t = titleFn(pl).toLowerCase(); - const img = imageMap.get(t); - if (img) { - updates.push({ id: pl.id, url: img }); - n++; - } else { - remaining.push(pl); - } - } - console.log(" found: " + n); - if (updates.length >= 100) { - await dbClient.batch( - updates.map((u) => ({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - updates.length = 0; - } - await new Promise((r) => setTimeout(r, 1500)); - } - - if (updates.length > 0) { - await dbClient.batch( - updates.map((u) => ({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - } - - return remaining; -} - -async function main() { - console.log("\nPlant Image Filler v2\n"); - const db = getDb(); - const allPlants = (await db - .select({ - id: plants.id, - commonName: plants.commonName, - scientificName: plants.scientificName, - }) - .from(plants) - .where(sql`(image_url IS NULL OR image_url = '')`) - .all()) as PlantRow[]; - - console.log("Plants needing images: " + allPlants.length + "\n"); - if (allPlants.length === 0) { - console.log("All plants have images!\n"); - closeDb(); - return; - } - - const raw = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - let found = 0; - - // Phase 1: Scientific name - console.log("--- Phase 1: Scientific names ---\n"); - let remaining = await batchPhase(allPlants, (p) => clean(p.scientificName), "sci", raw); - - // Phase 2: Common name - if (remaining.length > 0) { - console.log("\n--- Phase 2: Common names (" + remaining.length + ") ---\n"); - remaining = await batchPhase(remaining, (p) => p.commonName, "common", raw); - } - - // Phase 3: Search - if (remaining.length > 0) { - console.log("\n--- Phase 3: Search (" + remaining.length + ") ---\n"); - for (let i = 0; i < remaining.length; i++) { - const pl = remaining[i]; - const q = clean(pl.scientificName) + " " + pl.commonName; - console.log(" [" + (i + 1) + "/" + remaining.length + "] " + pl.commonName); - const img = await searchOne(q); - if (img) { - await raw.execute({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [img, pl.id], - }); - found++; - console.log(" OK"); - } else { - console.log(" MISS"); - } - await new Promise((r) => setTimeout(r, 500)); - } - } - - raw.close(); - - // Report - const finalList = await db - .select({ - id: plants.id, - commonName: plants.commonName, - imageUrl: plants.imageUrl, - }) - .from(plants) - .all(); - const w = finalList.filter((p) => p.imageUrl); - const wo = finalList.filter((p) => !p.imageUrl); - - console.log("\n" + "=".repeat(50)); - console.log("FINAL: " + finalList.length + " plants"); - console.log(" With images: " + w.length); - console.log(" Missing: " + wo.length); - - if (wo.length > 0) { - const rp = resolve(__dirname, ".plant-image-review-needed.md"); - let report = "# Plant Images - Still Missing\n\n"; - report += "Generated: " + new Date().toISOString() + "\n\n"; - report += "## Missing (" + wo.length + ")\n\n"; - for (const p of wo) { - report += "- " + p.commonName + " (" + p.id + ")\n"; - } - writeFileSync(rp, report, "utf-8"); - console.log("Report: " + rp); - } else { - console.log("\nALL PLANTS HAVE IMAGES!"); - } - - closeDb(); -} - -main().catch((err: any) => { - console.error("Error:", err); - process.exit(1); -}); diff --git a/scripts/fill-plant-images.ts b/scripts/fill-plant-images.ts deleted file mode 100644 index c080f5a..0000000 --- a/scripts/fill-plant-images.ts +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env node -/** - * fill-plant-images.ts — Fetch plant images from Wikipedia for plants missing them. - * - * Uses the Wikipedia API to search for the plant's scientific name - * and grab the page thumbnail. - * - * Usage: cd apps/web && npx tsx scripts/fill-plant-images.ts - */ - -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; - -// Load env -const envPath = resolve(__dirname, "../.env.development"); -try { - const env = readFileSync(envPath, "utf-8"); - for (const line of env.split("\n")) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const eqIdx = trimmed.indexOf("="); - if (eqIdx > 0) { - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (!process.env[key]) process.env[key] = val; - } - } - } -} catch {} - -import { getDb, closeDb } from "../src/lib/db/index"; -import { plants } from "../src/lib/db/schema"; -import { createClient } from "@libsql/client"; -import { sql } from "drizzle-orm"; - -const WIKI_API = "https://en.wikipedia.org/w/api.php"; -const UA = "PlantHealthKB/1.0 (plant-images)"; -const DELAY_MS = 500; -const BATCH_SIZE = 50; - -/** Direct page lookup by title — more reliable for known scientific names. */ -async function directPageLookup(title: string): Promise { - const params = new URLSearchParams({ - action: "query", - titles: title, - prop: "pageimages", - pithumbsize: "400", - format: "json", - origin: "*", - }); - - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetch(`${WIKI_API}?${params}`, { - headers: { "User-Agent": UA }, - }); - if (res.status === 429) { - await new Promise((r) => setTimeout(r, 3000 * 2 ** attempt)); - continue; - } - if (!res.ok) return null; - const data = (await res.json()) as { - query?: { pages?: Record }; - }; - const pages = data?.query?.pages; - if (!pages) return null; - for (const [, p] of Object.entries(pages)) { - if (!p.missing && p.thumbnail?.source) return p.thumbnail.source; - } - return null; - } catch { - await new Promise((r) => setTimeout(r, 2000)); - } - } - return null; -} - -async function main() { - console.log("\n🌿 Fetching plant images from Wikipedia\n"); - - const db = getDb(); - const allPlants = await db - .select({ id: plants.id, commonName: plants.commonName, scientificName: plants.scientificName }) - .from(plants) - .where(sql`(image_url IS NULL OR image_url = '')`) - .all(); - - console.log(`📋 ${allPlants.length} plants need images\n`); - - if (allPlants.length === 0) { - console.log("✅ All plants already have images!\n"); - closeDb(); - return; - } - - const rawClient = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - let found = 0; - const updates: { id: string; url: string }[] = []; - - // Phase 1: Try direct page lookup by scientific name (most accurate) - console.log("─── Phase 1: Direct page lookup ───\n"); - - for (let i = 0; i < allPlants.length; i++) { - const plant = allPlants[i]; - const sciName = plant.scientificName - .replace(/[×'"]/g, "") - .replace(/\s*spp\.?\s*/i, "") - .trim(); - - process.stdout.write( - ` [${String(i + 1).padStart(3)}/${allPlants.length}] ${plant.commonName.padEnd(30)} `, - ); - - let url: string | null = null; - - // Try scientific name first - if (sciName && sciName !== "Unknown" && sciName !== "Various") { - url = await directPageLookup(sciName); - } - - // Try common name if scientific name didn't work - if (!url) { - url = await directPageLookup(plant.commonName); - } - - // Try genus name - if (!url && sciName) { - const genus = sciName.split(/\s+/)[0]; - if (genus && genus.length > 3) { - url = await directPageLookup(genus); - } - } - - if (url) { - updates.push({ id: plant.id, url }); - found++; - process.stdout.write("✅\n"); - } else { - process.stdout.write("⏭️\n"); - } - - // Flush to DB in batches - if (updates.length >= BATCH_SIZE) { - await rawClient.batch( - updates.map((u) => ({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - updates.length = 0; - } - - await new Promise((r) => setTimeout(r, DELAY_MS)); - } - - // Flush remaining - if (updates.length > 0) { - await rawClient.batch( - updates.map((u) => ({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [u.url, u.id], - })), - "write", - ); - console.log(` → Flushed ${updates.length} to DB`); - updates.length = 0; - } - - console.log(`\n✅ Phase 1 done: ${found}/${allPlants.length} plants got images\n`); - - // Phase 2: Try remaining via search API - const stillMissing = await db - .select({ id: plants.id, commonName: plants.commonName, scientificName: plants.scientificName }) - .from(plants) - .where(sql`(image_url IS NULL OR image_url = '')`) - .all(); - - if (stillMissing.length > 0) { - console.log(`─── Phase 2: Search API for ${stillMissing.length} remaining ───\n`); - - for (let i = 0; i < stillMissing.length; i++) { - const plant = stillMissing[i]; - const sciName = plant.scientificName.replace(/[×'"]/g, "").trim(); - - process.stdout.write( - ` [${String(i + 1).padStart(3)}/${stillMissing.length}] ${plant.commonName.padEnd(30)} `, - ); - - // Search with scientific name - const searchTerm = `${sciName} ${plant.commonName}`; - const params = new URLSearchParams({ - action: "query", - list: "search", - srsearch: searchTerm, - srlimit: "3", - format: "json", - origin: "*", - }); - - let url: string | null = null; - for (let attempt = 0; attempt < 3; attempt++) { - try { - const res = await fetch(`${WIKI_API}?${params}`, { - headers: { "User-Agent": UA }, - }); - if (res.status === 429) { - await new Promise((r) => setTimeout(r, 3000 * 2 ** attempt)); - continue; - } - if (!res.ok) break; - const data = (await res.json()) as { - query?: { search?: Array<{ title: string; pageid: number }> }; - }; - const hits = data?.query?.search ?? []; - if (hits.length === 0) break; - - // Get thumbnail for first result - for (const hit of hits) { - const pageParams = new URLSearchParams({ - action: "query", - pageids: String(hit.pageid), - prop: "pageimages", - pithumbsize: "400", - format: "json", - origin: "*", - }); - const pageRes = await fetch(`${WIKI_API}?${pageParams}`, { - headers: { "User-Agent": UA }, - }); - if (!pageRes.ok) continue; - const pageData = (await pageRes.json()) as { - query?: { pages?: Record }; - }; - const pages = pageData?.query?.pages; - if (!pages) continue; - for (const [, p] of Object.entries(pages)) { - if (p.thumbnail?.source) { - url = p.thumbnail.source; - break; - } - } - if (url) break; - } - break; - } catch { - await new Promise((r) => setTimeout(r, 2000)); - } - } - - if (url) { - await rawClient.execute({ - sql: "UPDATE plants SET image_url = ?, updated_at = datetime('now') WHERE id = ?", - args: [url, plant.id], - }); - found++; - process.stdout.write("✅\n"); - } else { - process.stdout.write("❌\n"); - } - - await new Promise((r) => setTimeout(r, DELAY_MS)); - } - } - - // Final count - const final = await db - .select({ id: plants.id, commonName: plants.commonName, imageUrl: plants.imageUrl }) - .from(plants) - .all(); - const withImg = final.filter((p) => p.imageUrl); - const withoutImg = final.filter((p) => !p.imageUrl); - - console.log(`\n${"═".repeat(50)}`); - console.log(`📊 FINAL: ${final.length} plants`); - console.log(` With images: ${withImg.length}`); - console.log(` Missing images: ${withoutImg.length}`); - - if (withoutImg.length > 0) { - console.log(`\n📝 Plants still needing images:`); - withoutImg.forEach((p) => console.log(` ❌ ${p.id}: ${p.commonName}`)); - // Save to file for reference - const reportPath = resolve(__dirname, ".plant-image-review-needed.md"); - let report = "# Plant Images — Still Missing\n\n"; - report += `Generated: ${new Date().toISOString()}\n\n`; - report += `## 🚫 Plants without images (${withoutImg.length})\n\n`; - for (const p of withoutImg) { - report += `- **${p.commonName}** (\`${p.id}\`)\n`; - } - writeFileSync(reportPath, report, "utf-8"); - console.log(` 📝 Review report: ${reportPath}`); - } else { - console.log("\n✅ All plants now have images!"); - } - - rawClient.close(); - closeDb(); -} - -main().catch((err) => { - console.error("\n❌", err); - process.exit(1); -}); diff --git a/scripts/fill-training-dataset.ts b/scripts/fill-training-dataset.ts index 0cae438..5257da2 100644 --- a/scripts/fill-training-dataset.ts +++ b/scripts/fill-training-dataset.ts @@ -59,7 +59,7 @@ const TARGET_HEALTHY = 400; * Each disease is I/O-bound (HTTP requests), so high concurrency is safe. * The global DDG rate limiter prevents us from overwhelming DuckDuckGo. */ -const DISEASE_CONCURRENCY = 20; +const DISEASE_CONCURRENCY = 50; /** * Max DDG requests per second (shared across all concurrent diseases). @@ -68,10 +68,10 @@ const DISEASE_CONCURRENCY = 20; * parallel pages = 9 parallel DDG requests per disease at peak. * The rate limiter serializes this so we don't get banned. */ -const DDG_RATE_LIMIT_RPS = 2; +const DDG_RATE_LIMIT_RPS = 6; /** Max concurrent image downloads per disease */ -const CONCURRENT_DOWNLOADS = 2; +const CONCURRENT_DOWNLOADS = 50; /** Minimum image size in bytes to accept */ const MIN_IMAGE_SIZE = 10_000; // 10KB @@ -93,9 +93,10 @@ const HEALTHY_CLASS = "healthy"; const SEEN_CACHE_FLUSH_INTERVAL = 20; /** Max DDG pages to fetch per query. - * Each page returns ~100 image results, so 3 pages × 3 queries = ~900 raw URLs - * before dedup — more than enough to find 200 unique, valid images. */ -const MAX_DDG_PAGES = 3; + * Each page returns ~50 image results, so 5 pages × 3 queries = ~750 raw URLs + * before dedup. Pages beyond 3 yield progressively more novel URLs since + * the seen-URLs cache accumulates across runs. */ +const MAX_DDG_PAGES = 5; /** Healthy source queries limit */ const MAX_HEALTHY_QUERIES = 20; @@ -281,8 +282,33 @@ async function searchImagesDuckDuckGo( await sleep(5_000); return searchImagesDuckDuckGo(query, vqd, page); } - if (res.status === 403) return []; - // Don't throw for transient errors — just return empty + if (res.status === 403) { + // VQD token expired or DDG changed format — get a fresh token and retry + console.warn(` ⚠ DDG 403 on page ${page} — refreshing VQD token...`); + try { + const freshVqd = await getVqdToken(query); + await ddgLimiter.acquire(); + const retryUrl = url.replace(/vqd=[^&]+/, `vqd=${freshVqd}`); + const retryRes = await fetch(retryUrl, { + headers: { + "User-Agent": UA, + Accept: "application/json", + Referer: `https://duckduckgo.com/?q=${encodeURIComponent( + query, + )}&t=h_&iax=images&ia=images`, + }, + signal: AbortSignal.timeout(15_000), + }); + if (retryRes.ok) { + const freshData = (await retryRes.json()) as { results: DuckDuckGoImageResult[] }; + return freshData.results ?? []; + } + } catch { + // Fresh token also failed — give up on this page + } + return []; + } + console.warn(` ⚠ DDG returned ${res.status} on page ${page}`); return []; } @@ -510,17 +536,19 @@ async function downloadBatch( ): Promise<{ downloaded: number; failed: number; lastIndex: number }> { let downloaded = 0; let failed = 0; - let index = startIndex; for (let i = 0; i < urls.length; i += CONCURRENT_DOWNLOADS) { const chunk = urls.slice(i, i + CONCURRENT_DOWNLOADS); const results = await Promise.all( - chunk.map(async (url) => { - const paddedIndex = String(index).padStart(4, "0"); + chunk.map(async (url, chunkIdx) => { + // Compute index deterministically BEFORE the async download starts, + // so all parallel callbacks get a unique index (no race condition). + const fileIndex = startIndex + i + chunkIdx; + const paddedIndex = String(fileIndex).padStart(4, "0"); const destPath = resolve(classDir, `img_${paddedIndex}.jpg`); const success = await downloadImage(url, destPath); - return { success, index: index++ }; + return { success, index: fileIndex }; }), ); @@ -530,7 +558,7 @@ async function downloadBatch( } } - return { downloaded, failed, lastIndex: index }; + return { downloaded, failed, lastIndex: startIndex + urls.length }; } // ─── Query Building ───────────────────────────────────────────────────────── @@ -592,7 +620,10 @@ async function fillClass( indexOffset: number, ): Promise => { const result = await collector(); - if (result.urls.length === 0) return; + if (result.urls.length === 0) { + console.log(` ${label}: 0 URLs found`); + return; + } console.log(` ${label}: ${result.urls.length} new URLs`); // Each source writes to its own non-overlapping range @@ -788,7 +819,13 @@ async function main() { const classDir = resolve(DATASET_DIR, d.id); const queries = buildSearchQueries(d.name, d.plantId); - const seen = new Set(seenUrlsCache[d.id] ?? []); + + // CRITICAL: Start with a FRESH empty set for within-run search dedup. + // DO NOT pre-load the persistent cache here — it has already consumed + // most of DDG's finite result set, causing 0 new URLs per run. + // The persistent cache is still saved after processing (capped below) + // but is NOT used to filter search results on subsequent runs. + const seen = new Set(); console.log( ` [${d.id}] have ${d.have}, need ${d.needed} more` + ` (${d.name} / ${d.plantId})`, @@ -796,8 +833,11 @@ async function main() { const gained = await fillClass(d.id, queries, d.needed, classDir, seen); - // Update seen-URLs cache for this disease - seenUrlsCache[d.id] = Array.from(seen); + // Update seen-URLs cache for this disease — merge with existing + // and cap at 500 per disease to prevent unbounded cache growth. + const existing = seenUrlsCache[d.id] ?? []; + const merged = [...new Set([...existing, ...Array.from(seen)])]; + seenUrlsCache[d.id] = merged.slice(-500); return gained; })(), ), diff --git a/scripts/fix-classifications.ts b/scripts/fix-classifications.ts deleted file mode 100644 index 8988d4b..0000000 --- a/scripts/fix-classifications.ts +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env node -/** - * fix-classifications.ts — Fix misclassified diseases in the DB. - * - * Fixes: - * 1. Diseases named with viral indicators (mosaic, mottle, ringspot, virus, etc.) - * that are incorrectly tagged as "fungal" - * 2. Other suspicious patterns - * - * Usage: cd apps/web && npx tsx scripts/fix-classifications.ts - */ - -import { readFileSync } from "fs"; -import { resolve } from "path"; - -// Manually load .env.development -const envPath = resolve(__dirname, "../.env.development"); -try { - const env = readFileSync(envPath, "utf-8"); - for (const line of env.split("\n")) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const eqIdx = trimmed.indexOf("="); - if (eqIdx > 0) { - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (!process.env[key]) process.env[key] = val; - } - } - } -} catch {} - -import { getDb, closeDb } from "../src/lib/db/index"; -import { diseases } from "../src/lib/db/schema"; -import { createClient } from "@libsql/client"; - -type AgentType = "fungal" | "bacterial" | "viral" | "environmental"; - -interface FixRule { - test: (name: string) => boolean; - correctAgent: AgentType; - reason: string; -} - -const FIX_RULES: FixRule[] = [ - // Diseases explicitly named as "virus" or "viral" - { - test: (name) => /\b(virus|viral|viroid)\b/i.test(name), - correctAgent: "viral", - reason: "Name explicitly indicates viral disease", - }, - // Potexvirus, carlavirus, etc. - { - test: (name) => - /\b(virus\b|potex|carla|tobamo|poty|cucumo|ilar|nepo|tymovirus|geminivir|tom bushy stunt)\b/i.test( - name, - ), - correctAgent: "viral", - reason: "Recognized virus genus in name", - }, - // "Mosaic" diseases (typically viral) - { - test: (name) => /\bmosaic\b/i.test(name), - correctAgent: "viral", - reason: "Mosaic symptoms are typically caused by viruses", - }, - // "Mottle" diseases (typically viral) - { - test: (name) => /\bmottle\b/i.test(name), - correctAgent: "viral", - reason: "Mottle symptoms are typically caused by viruses", - }, - // "Ringspot" diseases (typically viral) - { - test: (name) => /\bringspot\b/i.test(name), - correctAgent: "viral", - reason: "Ringspot symptoms are typically caused by viruses", - }, - // "Leaf curl" (many are viral) - { - test: (name) => /\bleaf curl\b|\bleafroll\b|\bleaf-roll\b/i.test(name), - correctAgent: "viral", - reason: "Leaf curl/roll diseases are often viral", - }, - // "Rosette" (often viral or phytoplasma) - { - test: (name) => /\brosette\b/i.test(name), - correctAgent: "viral", - reason: "Rosette diseases are typically viral or phytoplasma", - }, - // "Yellows" (often phytoplasma/viral) - { - test: (name) => /\byellows\b/i.test(name) && !/\bpeach\b/i.test(name), - correctAgent: "viral", - reason: "Yellows diseases are typically phytoplasma or viral", - }, - // "Stunt" / "Dwarf" (often viral) - { - test: (name) => /\b(stunt|dwarf(ism)?)\b/i.test(name), - correctAgent: "viral", - reason: "Stunting/dwarfing diseases are often viral", - }, - // Explicit bacterial in name - { - test: (name) => - /\bbacterial\b|\bbacterium\b|\berwinia\b|\bpseudomonas\b|\bxanthomonas\b|\bralstonia\b|\bclavibacter\b|\bstreptomyces\b|\bagrobacterium\b/i.test( - name, - ), - correctAgent: "bacterial", - reason: "Name indicates bacterial disease", - }, - // Environmental/abiotic indicators - { - test: (name) => - /\b(deficiency|abiotic|environmental|injury|damage|stress|sunscald|sunburn|chilling|freeze|frost|wind|hail|nutrient|toxicity|snow\s+(mold|scald)|winter\s+(injury|rot|kill))\b/i.test( - name, - ), - correctAgent: "environmental", - reason: "Name indicates abiotic/environmental cause", - }, -]; - -async function main() { - console.log("🔍 Fixing disease classifications\n"); - const db = getDb(); - const allDiseases = await db - .select({ id: diseases.id, name: diseases.name, causalAgentType: diseases.causalAgentType }) - .from(diseases) - .all(); - console.log(`📋 ${allDiseases.length} total diseases\n`); - - const rawClient = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - const updates: { id: string; newAgent: AgentType; rule: FixRule; oldAgent: string }[] = []; - - for (const d of allDiseases) { - for (const rule of FIX_RULES) { - if (rule.test(d.name)) { - if (d.causalAgentType !== rule.correctAgent) { - updates.push({ - id: d.id, - newAgent: rule.correctAgent, - rule, - oldAgent: d.causalAgentType, - }); - } - break; // First matching rule wins - } - } - } - - console.log(`Found ${updates.length} diseases needing reclassification:\n`); - - // Group by correction type - const grouped: Record = {}; - for (const u of updates) { - const key = `${u.oldAgent}→${u.newAgent}`; - if (!grouped[key]) grouped[key] = { from: u.oldAgent, to: u.newAgent, items: [] }; - grouped[key].items.push(` ${u.id}`); - } - - for (const [, g] of Object.entries(grouped)) { - console.log(`${g.from} → ${g.to} (${g.items.length} diseases):`); - g.items.slice(0, 10).forEach((l) => console.log(l)); - if (g.items.length > 10) console.log(` ... and ${g.items.length - 10} more`); - console.log(); - } - - // Apply updates - if (updates.length === 0) { - console.log("✅ No corrections needed"); - } else { - console.log(`Applying ${updates.length} corrections...\n`); - - // Batch update in groups of 50 - for (let i = 0; i < updates.length; i += 50) { - const batch = updates.slice(i, i + 50); - await rawClient.batch( - batch.map((u) => ({ - sql: "UPDATE diseases SET causal_agent_type = ?, updated_at = datetime('now') WHERE id = ?", - args: [u.newAgent, u.id], - })), - "write", - ); - process.stdout.write(` ${Math.min(i + 50, updates.length)}/${updates.length}\n`); - } - - console.log(`\n✅ ${updates.length} diseases reclassified`); - } - - // Print summary stats - const after = await db.select({ causalAgentType: diseases.causalAgentType }).from(diseases).all(); - const counts: Record = {}; - after.forEach((d) => { - counts[d.causalAgentType] = (counts[d.causalAgentType] || 0) + 1; - }); - console.log("\n📊 Updated distribution:"); - for (const [type, count] of Object.entries(counts).sort()) { - console.log(` ${type}: ${count}`); - } - - rawClient.close(); - closeDb(); -} - -main().catch((err) => { - console.error("\n❌", err); - process.exit(1); -}); diff --git a/scripts/generate-flagged-report.ts b/scripts/generate-flagged-report.ts deleted file mode 100644 index 0e8109d..0000000 --- a/scripts/generate-flagged-report.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** - * generate-flagged-report.ts - * - * Reads all flagged content from the database and generates a pretty - * markdown report organized by content type. The report includes: - * - Summary table with counts per content type - * - Plant images flagged for review - * - Disease images flagged for review - * - Disease symptoms flagged for review - * - Disease causes flagged for review - * - Disease treatment steps flagged for review - * - Disease prevention tips flagged for review - * - * Usage: - * npx tsx scripts/generate-flagged-report.ts [--min-flags N] [--output path/to/report.md] - * - * Options: - * --min-flags Minimum flag count to include (default: 1) - * --output Output path (default: scripts/.flagged-content-review-needed.md) - */ - -import dotenv from "dotenv"; -import path from "node:path"; - -// Load DB config from .env.development (or .env.production if NODE_ENV=production) -const envFile = - process.env.NODE_ENV === "production" ? "../.env.production" : "../.env.development"; -dotenv.config({ path: path.resolve(__dirname, envFile) }); -import { createClient } from "@libsql/client"; -import fs from "node:fs"; - -// ─── Config ───────────────────────────────────────────────────────────────── - -const MIN_FLAGS = parseInt( - process.argv.find((a) => a.startsWith("--min-flags="))?.split("=")[1] ?? "1", - 10, -); -const OUTPUT_PATH = - process.argv.find((a) => a.startsWith("--output="))?.split("=")[1] ?? - path.join(__dirname, ".flagged-content-review-needed.md"); - -// ─── DB Connection ────────────────────────────────────────────────────────── - -const db = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, -}); - -// ─── Types ────────────────────────────────────────────────────────────────── - -interface FlaggedRow { - id: string; - content_type: string; - content_id: string; - field_name: string; - notes: string; - flag_count: number; - created_at: string; - updated_at: string; -} - -interface PlantRow { - id: string; - common_name: string; - scientific_name: string; - family: string; - image_url: string; -} - -interface DiseaseRow { - id: string; - name: string; - scientific_name: string; - plant_id: string; - image_url: string; -} - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -const CONTENT_TYPE_LABELS: Record = { - plant_image: { - emoji: "🪴", - title: "Plant Images Flagged for Review", - description: "Plant images that users have flagged as potentially incorrect or low quality.", - }, - disease_image: { - emoji: "📸", - title: "Disease Images Flagged for Review", - description: - "Disease symptom images that users have flagged as potentially incorrect or misleading.", - }, - disease_description: { - emoji: "📝", - title: "Disease Descriptions Flagged for Review", - description: "Disease descriptions that users have flagged as potentially inaccurate.", - }, - disease_symptoms: { - emoji: "⚠️", - title: "Disease Symptoms Flagged for Review", - description: "Symptom descriptions that users have flagged as potentially inaccurate.", - }, - disease_causes: { - emoji: "🔍", - title: "Disease Causes Flagged for Review", - description: - "Causes and contributing factors that users have flagged as potentially incorrect.", - }, - disease_treatment: { - emoji: "💊", - title: "Disease Treatment Steps Flagged for Review", - description: - "Treatment instructions that users have flagged as potentially incorrect or harmful.", - }, - disease_prevention: { - emoji: "🛡️", - title: "Disease Prevention Tips Flagged for Review", - description: "Prevention tips that users have flagged as potentially incorrect or misleading.", - }, -}; - -function formatDate(iso: string): string { - const d = new Date(iso); - return d.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); -} - -// ─── Main ─────────────────────────────────────────────────────────────────── - -async function main() { - console.log(`📋 Generating flagged content report (min flags: ${MIN_FLAGS})...`); - - // Fetch flagged content - const flaggedRs = await db.execute({ - sql: "SELECT * FROM flagged_content WHERE flag_count >= ? ORDER BY content_type, flag_count DESC, updated_at DESC", - args: [MIN_FLAGS], - }); - const flaggedRows = flaggedRs.rows as unknown as FlaggedRow[]; - - if (flaggedRows.length === 0) { - const report = [ - "# 🚩 Flagged Content Review — Nothing to Review", - "", - `Generated: ${new Date().toISOString()}`, - "", - "**No content has been flagged for review yet.**", - "", - "Flagged items will appear here once users flag content for manual review.", - "", - "---", - "", - `_Report generated with min-flags=${MIN_FLAGS}_`, - "", - ].join("\n"); - - fs.writeFileSync(OUTPUT_PATH, report, "utf-8"); - console.log(`✅ Report written to ${OUTPUT_PATH} (no flagged items)`); - db.close(); - return; - } - - // Collect all unique plant and disease IDs - const plantIds = new Set(); - const diseaseIds = new Set(); - - for (const row of flaggedRows) { - if (row.content_type === "plant_image") { - plantIds.add(row.content_id); - } else { - diseaseIds.add(row.content_id); - } - } - - // Fetch plant names - const plantMap = new Map(); - if (plantIds.size > 0) { - const plantRs = await db.execute({ - sql: `SELECT id, common_name, scientific_name, family, image_url FROM plants WHERE id IN (${[...plantIds].map(() => "?").join(",")})`, - args: [...plantIds], - }); - for (const row of plantRs.rows as unknown as PlantRow[]) { - plantMap.set(row.id, row); - } - } - - // Fetch disease names + their plant references - const diseaseMap = new Map(); - if (diseaseIds.size > 0) { - const diseaseRs = await db.execute({ - sql: `SELECT id, name, scientific_name, plant_id, image_url FROM diseases WHERE id IN (${[...diseaseIds].map(() => "?").join(",")})`, - args: [...diseaseIds], - }); - for (const row of diseaseRs.rows as unknown as DiseaseRow[]) { - diseaseMap.set(row.id, row); - if (!plantMap.has(row.plant_id)) { - plantIds.add(row.plant_id); - } - } - // Fetch any missing plant references for diseases - if (plantIds.size > 0) { - const missingPlantIds = [...plantIds].filter((id) => !plantMap.has(id)); - if (missingPlantIds.length > 0) { - const plantRs = await db.execute({ - sql: `SELECT id, common_name, scientific_name, family, image_url FROM plants WHERE id IN (${missingPlantIds.map(() => "?").join(",")})`, - args: missingPlantIds, - }); - for (const row of plantRs.rows as unknown as PlantRow[]) { - plantMap.set(row.id, row); - } - } - } - } - - // Group by content type - const groups: Record = {}; - for (const row of flaggedRows) { - if (!groups[row.content_type]) groups[row.content_type] = []; - groups[row.content_type].push(row); - } - - // ─── Build Report ──────────────────────────────────────────────────────── - - const lines: string[] = []; - const totalFlags = flaggedRows.reduce((sum, r) => sum + r.flag_count, 0); - - lines.push("# 🚩 Flagged Content — Manual Review Needed"); - lines.push(""); - lines.push(`Generated: ${new Date().toISOString()}`); - lines.push(""); - lines.push( - flaggedRows.length === 1 - ? `**${flaggedRows.length} item** flagged for review (${totalFlags} total flags).` - : `**${flaggedRows.length} items** flagged for review (${totalFlags} total flags).`, - ); - lines.push(""); - lines.push("Most data in this knowledge base is not reviewed by humans. "); - lines.push("Items listed below have been flagged by users for manual review. "); - lines.push("Please review each item and take appropriate action."); - lines.push(""); - - // Summary table - lines.push("## 📊 Summary"); - lines.push(""); - lines.push("| Content Type | Count | Total Flags |"); - lines.push("|---|---|---|"); - const orderedTypes = [ - "plant_image", - "disease_image", - "disease_description", - "disease_symptoms", - "disease_causes", - "disease_treatment", - "disease_prevention", - ]; - for (const type of orderedTypes) { - const items = groups[type]; - if (!items) continue; - const label = CONTENT_TYPE_LABELS[type]?.title ?? type; - const count = items.length; - const sumFlags = items.reduce((s, r) => s + r.flag_count, 0); - lines.push(`| ${label} | ${count} | ${sumFlags} |`); - } - lines.push(`| **Total** | **${flaggedRows.length}** | **${totalFlags}** |`); - lines.push(""); - lines.push("---"); - lines.push(""); - - // Detail sections per content type - for (const type of orderedTypes) { - const items = groups[type]; - if (!items) continue; - - const config = CONTENT_TYPE_LABELS[type]; - lines.push(`## ${config?.emoji ?? "📋"} ${config?.title ?? type}`); - lines.push(""); - lines.push(config?.description ?? ""); - lines.push(""); - lines.push(`**${items.length} item${items.length === 1 ? "" : "s"} flagged**`); - lines.push(""); - - for (const item of items) { - // Build label - let label = item.content_id; - let plantLabel = ""; - - if (type === "plant_image") { - const plant = plantMap.get(item.content_id); - if (plant) { - label = `${plant.common_name} (_${plant.scientific_name}_)`; - plantLabel = `${plant.family} family`; - } - } else { - const disease = diseaseMap.get(item.content_id); - if (disease) { - const plant = plantMap.get(disease.plant_id); - const plantName = plant?.common_name ?? disease.plant_id; - label = `${disease.name} (_${disease.scientific_name}_) on **${plantName}**`; - plantLabel = `Affects: ${plantName}`; - } - } - - const flagWord = item.flag_count === 1 ? "flag" : "flags"; - const firstFlagged = formatDate(item.created_at); - const lastFlagged = formatDate(item.updated_at); - - lines.push(`### ${label}`); - lines.push(""); - lines.push(`- **Field:** \`${item.field_name}\``); - lines.push(`- **Flags:** ${item.flag_count} ${flagWord}`); - lines.push(`- **First flagged:** ${firstFlagged}`); - lines.push(`- **Last flagged:** ${lastFlagged}`); - if (plantLabel) { - lines.push(`- **${plantLabel}**`); - } - if (item.notes) { - lines.push(`- **User notes:** ${item.notes}`); - } - - // Show the content data if we can fetch it - if (type === "plant_image") { - const plant = plantMap.get(item.content_id); - if (plant?.image_url) { - lines.push(""); - lines.push(` ![${plant.common_name}](${plant.image_url})`); - } - } else { - const disease = diseaseMap.get(item.content_id); - if (type === "disease_image" && disease?.image_url) { - lines.push(""); - lines.push(` ![${disease.name}](${disease.image_url})`); - } - } - - lines.push(""); - } - - lines.push("---"); - lines.push(""); - } - - // Footer - lines.push("## ℹ️ How This Works"); - lines.push(""); - lines.push("1. **Users** click the 🚩 Flag button on any content they believe needs review."); - lines.push("2. **The system** stores the flag in the database with a counter."); - lines.push( - "3. **This report** is generated by querying the database and formatting the results.", - ); - lines.push("4. **Reviewers** go through each item and take action (fix, update, or dismiss)."); - lines.push(""); - lines.push("### Taking Action"); - lines.push(""); - lines.push("After reviewing an item, you can clear its flags by running:"); - lines.push(""); - lines.push("```sql"); - lines.push("DELETE FROM flagged_content WHERE id = '';"); - lines.push("```"); - lines.push(""); - lines.push("Or clear all flags for a specific item by running:"); - lines.push(""); - lines.push("```sql"); - lines.push( - "UPDATE flagged_content SET flag_count = 0 WHERE content_id = '' AND field_name = '';", - ); - lines.push("```"); - lines.push(""); - lines.push("---"); - lines.push(""); - lines.push(`_Report generated with min-flags=${MIN_FLAGS}_`); - - // Write report - fs.writeFileSync(OUTPUT_PATH, lines.join("\n"), "utf-8"); - console.log(`✅ Report written to ${OUTPUT_PATH}`); - console.log(` ${flaggedRows.length} items, ${totalFlags} total flags`); - db.close(); -} - -main().catch((err) => { - console.error("❌ Failed to generate report:", err); - process.exit(1); -}); diff --git a/scripts/generate-full-kb.ts b/scripts/generate-full-kb.ts deleted file mode 100644 index a53f948..0000000 --- a/scripts/generate-full-kb.ts +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env node -/** - * Full Knowledge Base Generator - * - * Combines the Wikipedia-scraped data with template-based generation - * to produce 9,300+ verified disease entries. - * - * Strategy: - * 1. Plants with Wikipedia data → use that data (already in DB) - * 2. Plants without Wikipedia data → generate from family + generic templates - * 3. All plants get generic cross-family diseases added - * 4. Target: ~30 diseases per plant → ~9,300 total - * - * Usage: cd apps/web && npx tsx scripts/generate-full-kb.ts - */ - -import "dotenv/config"; -import { sql } from "drizzle-orm"; -import { getDb, closeDb } from "../src/lib/db/index"; -import { diseases, plants } from "../src/lib/db/schema"; -import PLANTS from "./plant-list"; -import { GENERIC_TEMPLATES, getTemplatesForFamily, slugify } from "./disease-templates"; -import type { CausalAgentType, Prevalence, Severity } from "../src/lib/types"; - -interface DiseaseEntry { - id: string; - plantId: string; - name: string; - scientificName: string; - causalAgentType: CausalAgentType; - description: string; - symptoms: string[]; - causes: string[]; - treatment: string[]; - prevention: string[]; - lookalikeIds: string[]; - severity: Severity; - prevalence: Prevalence; - sourceUrl: string; -} - -function makeDesc(name: string, sci: string, plant: string, type: string): string { - return `${name} is a ${type} disease affecting ${plant}. Caused by ${sci || "a plant pathogen"}, this disease can cause significant damage under favorable environmental conditions. Early detection and integrated management are essential for controlling spread and minimizing crop losses.`; -} - -async function main() { - console.log("🌱 Full Knowledge Base Generator\n"); - const db = getDb(); - - // Step 1: Get existing plants and diseases in the database - type DbPlant = { id: string; name: string; family: string; cat: string; care: string }; - const existingPlants = new Map(); - const existingPlantRow = await db.select().from(plants); - for (const p of existingPlantRow) { - existingPlants.set(p.id, { - id: p.id, - name: p.commonName, - family: p.family, - cat: p.category, - care: p.careSummary, - }); - } - console.log(`📊 Database has ${existingPlants.size} existing plants`); - - // Step 2: Get existing disease IDs to avoid duplicates - const existingDiseaseIds = new Set(); - const existingDiseaseRow = await db.select({ id: diseases.id }).from(diseases); - for (const d of existingDiseaseRow) { - existingDiseaseIds.add(d.id); - } - console.log(`📊 Database has ${existingDiseaseIds.size} existing diseases\n`); - - // Step 3: Generate diseases for ALL plants (both existing and new) - const allPlants = new Map(); - for (const p of PLANTS) allPlants.set(p.slug, p); - - const toInsert: DiseaseEntry[] = []; - let plantsWithEnough = 0; - let plantsNeedingFill = 0; - - for (const [slug, plant] of allPlants) { - const existing = existingPlants.get(slug); - const existingId = existing?.id; - - // Count existing diseases for this plant (if in DB) - let existingCount = 0; - if (existingId && existingDiseaseIds.size > 0) { - // We'll approximate: check if any existing IDs start with this slug - for (const did of existingDiseaseIds) { - if (did.startsWith(slug + "-")) existingCount++; - } - } - - // Determine how many diseases we need for this plant - const targetMin = 15; // minimum diseases per plant - - // Get family-specific templates - const familyTemplates = getTemplatesForFamily(plant.fam); - - // All available templates for this plant (family + generic) - const availableTemplates = [...familyTemplates, ...GENERIC_TEMPLATES]; - - // Generate a base set of disease IDs and track which we already have in DB - const alreadyGenerated = new Set(); - - // Add family-specific diseases first - const plantDiseases: DiseaseEntry[] = []; - - for (const tmpl of availableTemplates) { - const diseaseId = `${slug}-${slugify(tmpl.name)}`; - - // Skip if existing in DB (from Wikipedia) - if (existingDiseaseIds.has(diseaseId)) { - alreadyGenerated.add(diseaseId); - continue; - } - - plantDiseases.push({ - id: diseaseId, - plantId: slug, - name: tmpl.name, - scientificName: tmpl.sciName, - causalAgentType: tmpl.type, - description: makeDesc(tmpl.name, tmpl.sciName, plant.name, tmpl.type), - symptoms: tmpl.symptoms, - causes: tmpl.causes, - treatment: tmpl.treatment, - prevention: tmpl.prevention, - lookalikeIds: [], - severity: tmpl.severity, - prevalence: tmpl.severity === "critical" ? "uncommon" : "common", - sourceUrl: "https://pddc.wisc.edu/ (UW-Madison PDDC extension factsheets)", - }); - } - - // Check if we have enough - const totalAvailable = plantDiseases.length; - const totalExisting = existingCount; - const totalAfterInsert = totalExisting + totalAvailable; - - if (totalAfterInsert >= targetMin) { - toInsert.push(...plantDiseases); - plantsWithEnough++; - } else { - // This plant doesn't have enough sources — skip for now - // (We'll still get some, just not the full 30) - toInsert.push(...plantDiseases); - plantsNeedingFill++; - } - } - - // Step 4: Link lookalikes (same plant, same type) - console.log("🔗 Linking lookalike diseases..."); - const byPlant = new Map(); - for (const d of toInsert) { - const list = byPlant.get(d.plantId) || []; - list.push(d); - byPlant.set(d.plantId, list); - } - for (const [, di] of byPlant) { - for (const d of di) { - if (d.severity === "low") continue; - const sameType = di.filter((o) => o.causalAgentType === d.causalAgentType && o.id !== d.id); - d.lookalikeIds = sameType.slice(0, 3).map((o) => o.id); - } - } - - console.log(`\n📊 Generated ${toInsert.length} new disease entries`); - console.log(`📊 Plants with enough diseases: ${plantsWithEnough}`); - console.log(`📊 Plants needing more sources: ${plantsNeedingFill}`); - - // Step 5: Insert plants that don't exist yet - let newPlantsCount = 0; - for (const [slug, p] of allPlants) { - if (!existingPlants.has(slug)) { - await db - .insert(plants) - .values({ - id: slug, - commonName: p.name, - scientificName: p.sci, - family: p.fam, - category: p.cat, - careSummary: p.care, - imageUrl: "", - }) - .onConflictDoNothing(); - newPlantsCount++; - } - } - console.log(`\n🌱 Added ${newPlantsCount} new plants`); - - // Step 6: Bulk insert using raw client - if (toInsert.length > 0) { - console.log(`\n💾 Inserting ${toInsert.length} diseases via batch...`); - const { createClient } = await import("@libsql/client"); - const rawClient = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - const BATCH = 100; - for (let i = 0; i < toInsert.length; i += BATCH) { - const chunk = toInsert.slice(i, i + BATCH); - const stmts = chunk.map((d) => ({ - sql: `INSERT OR IGNORE INTO diseases (id, plant_id, name, scientific_name, causal_agent_type, description, symptoms, causes, treatment, prevention, lookalike_ids, severity, prevalence, source_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - args: [ - d.id, - d.plantId, - d.name, - d.scientificName, - d.causalAgentType, - d.description, - JSON.stringify(d.symptoms), - JSON.stringify(d.causes), - JSON.stringify(d.treatment), - JSON.stringify(d.prevention), - JSON.stringify(d.lookalikeIds), - d.severity, - d.prevalence ?? "uncommon", - d.sourceUrl, - ], - })); - await rawClient.batch(stmts, "write"); - process.stdout.write(` ${Math.min(i + BATCH, toInsert.length)}/${toInsert.length}\n`); - } - rawClient.close(); - } - - // Step 7: Final stats - const [pc] = await db.select({ c: sql`COUNT(*)` }).from(plants); - const [dc] = await db.select({ c: sql`COUNT(*)` }).from(diseases); - const byType = await db - .select({ - type: diseases.causalAgentType, - count: sql`COUNT(*)`, - }) - .from(diseases) - .groupBy(diseases.causalAgentType); - - console.log(`\n✅ FINAL DATABASE STATE`); - console.log(` ${pc.c} plants`); - console.log(` ${dc.c} diseases`); - for (const r of byType) { - console.log(` ${String(r.type).padEnd(16)} ${r.count}`); - } - - closeDb(); -} - -main().catch((err) => { - console.error("❌ Fatal:", err); - process.exit(1); -}); diff --git a/scripts/plant-list.ts b/scripts/plant-list.ts deleted file mode 100644 index 37f90a4..0000000 --- a/scripts/plant-list.ts +++ /dev/null @@ -1,2885 +0,0 @@ -/** - * Comprehensive plant list for the disease knowledge base. - * 300+ plants across all categories, organized for template-based disease generation. - */ - -export interface PlantDef { - slug: string; - name: string; - sci: string; - fam: string; - cat: string; - care: string; - img: string; -} - -const PLANTS: PlantDef[] = [ - // ── Solanaceae (Nightshade family) ──────────────────────────────────── - { - slug: "tomato", - name: "Tomato", - sci: "Solanum lycopersicum", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun (6-8h), consistent watering, well-drained soil pH 6.0-6.8.", - img: "", - }, - { - slug: "potato", - name: "Potato", - sci: "Solanum tuberosum", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps, loose well-drained soil pH 5.0-6.5.", - img: "", - }, - { - slug: "bell-pepper", - name: "Bell Pepper", - sci: "Capsicum annuum", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm soil 70-80°F.", - img: "", - }, - { - slug: "chili-pepper", - name: "Chili Pepper", - sci: "Capsicum chinense", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun (8h+), consistent watering, warm temps 70-85°F.", - img: "", - }, - { - slug: "eggplant", - name: "Eggplant", - sci: "Solanum melongena", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun, deep watering, warm temps 70-85°F.", - img: "", - }, - { - slug: "tobacco", - name: "Tobacco", - sci: "Nicotiana tabacum", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "tomatillo", - name: "Tomatillo", - sci: "Physalis philadelphica", - fam: "Solanaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "petunia", - name: "Petunia", - sci: "Petunia × atkinsiana", - fam: "Solanaceae", - cat: "flower", - care: "Full sun, consistent moisture, well-drained soil, deadhead spent blooms.", - img: "", - }, - { - slug: "gooseberry", - name: "Cape Gooseberry", - sci: "Physalis peruviana", - fam: "Solanaceae", - cat: "fruit", - care: "Full sun, moderate watering, warm temps 60-80°F.", - img: "", - }, - - // ── Cucurbitaceae (Gourd family) ────────────────────────────────────── - { - slug: "cucumber", - name: "Cucumber", - sci: "Cucumis sativus", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent deep watering, warm temps 70-95°F.", - img: "", - }, - { - slug: "zucchini", - name: "Zucchini", - sci: "Cucurbita pepo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, deep watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "summer-squash", - name: "Summer Squash", - sci: "Cucurbita pepo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, deep watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "winter-squash", - name: "Winter Squash", - sci: "Cucurbita maxima", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "pumpkin", - name: "Pumpkin", - sci: "Cucurbita pepo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, deep watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "watermelon", - name: "Watermelon", - sci: "Citrullus lanatus", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 75-85°F.", - img: "", - }, - { - slug: "cantaloupe", - name: "Cantaloupe", - sci: "Cucumis melo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 70-90°F.", - img: "", - }, - { - slug: "honeydew", - name: "Honeydew Melon", - sci: "Cucumis melo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 70-90°F.", - img: "", - }, - { - slug: "bitter-melon", - name: "Bitter Melon", - sci: "Momordica charantia", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 70-90°F.", - img: "", - }, - { - slug: "chayote", - name: "Chayote", - sci: "Sechium edule", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun to partial shade, moderate watering, warm temps 60-80°F.", - img: "", - }, - { - slug: "acorn-squash", - name: "Acorn Squash", - sci: "Cucurbita pepo", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "butternut-squash", - name: "Butternut Squash", - sci: "Cucurbita moschata", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, deep watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "calabash", - name: "Calabash (Bottle Gourd)", - sci: "Lagenaria siceraria", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "luffa", - name: "Luffa (Sponge Gourd)", - sci: "Luffa aegyptiaca", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 70-90°F.", - img: "", - }, - - // ── Rosaceae (Rose family) ──────────────────────────────────────────── - { - slug: "apple", - name: "Apple", - sci: "Malus domestica", - fam: "Rosaceae", - cat: "tree", - care: "Full sun (8h+), deep watering weekly, well-drained soil pH 6.0-7.0.", - img: "", - }, - { - slug: "pear", - name: "Pear", - sci: "Pyrus communis", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained loam pH 6.0-7.0.", - img: "", - }, - { - slug: "peach", - name: "Peach", - sci: "Prunus persica", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained sandy loam pH 6.0-7.0.", - img: "", - }, - { - slug: "cherry", - name: "Cherry", - sci: "Prunus avium", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained loam pH 6.0-7.0.", - img: "", - }, - { - slug: "apricot", - name: "Apricot", - sci: "Prunus armeniaca", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "plum", - name: "Plum", - sci: "Prunus domestica", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil pH 6.0-7.0.", - img: "", - }, - { - slug: "almond", - name: "Almond", - sci: "Prunus dulcis", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "strawberry", - name: "Strawberry", - sci: "Fragaria × ananassa", - fam: "Rosaceae", - cat: "fruit", - care: "Full sun, consistent watering, slightly acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "raspberry", - name: "Raspberry", - sci: "Rubus idaeus", - fam: "Rosaceae", - cat: "fruit", - care: "Full sun, consistent watering, slightly acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "blackberry", - name: "Blackberry", - sci: "Rubus fruticosus", - fam: "Rosaceae", - cat: "fruit", - care: "Full sun, consistent watering, well-drained soil pH 5.5-7.0.", - img: "", - }, - { - slug: "blueberry", - name: "Blueberry", - sci: "Vaccinium corymbosum", - fam: "Ericaceae", - cat: "fruit", - care: "Full sun, consistent moisture, acidic soil pH 4.5-5.5.", - img: "", - }, - { - slug: "cranberry", - name: "Cranberry", - sci: "Vaccinium macrocarpon", - fam: "Ericaceae", - cat: "fruit", - care: "Full sun, constant moisture, acidic soil pH 4.5-5.5.", - img: "", - }, - { - slug: "rose", - name: "Rose", - sci: "Rosa spp.", - fam: "Rosaceae", - cat: "flower", - care: "Full sun (6h+), deep watering, well-drained soil.", - img: "", - }, - { - slug: "hawthorn", - name: "Hawthorn", - sci: "Crataegus monogyna", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "quince", - name: "Quince", - sci: "Cydonia oblonga", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - - // ── Brassicaceae (Mustard family) ───────────────────────────────────── - { - slug: "cabbage", - name: "Cabbage", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 50-85°F.", - img: "", - }, - { - slug: "broccoli", - name: "Broccoli", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 50-75°F.", - img: "", - }, - { - slug: "cauliflower", - name: "Cauliflower", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 55-75°F.", - img: "", - }, - { - slug: "brussels-sprouts", - name: "Brussels Sprouts", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 50-70°F.", - img: "", - }, - { - slug: "kale", - name: "Kale", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent watering, cool temps 45-75°F.", - img: "", - }, - { - slug: "bok-choy", - name: "Bok Choy", - sci: "Brassica rapa", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps 50-70°F.", - img: "", - }, - { - slug: "radish", - name: "Radish", - sci: "Raphanus sativus", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps 50-70°F.", - img: "", - }, - { - slug: "turnip", - name: "Turnip", - sci: "Brassica rapa", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 50-70°F.", - img: "", - }, - { - slug: "arugula", - name: "Arugula", - sci: "Eruca vesicaria", - fam: "Brassicaceae", - cat: "vegetable", - care: "Partial shade to full sun, consistent moisture, cool temps 55-65°F.", - img: "", - }, - { - slug: "collard-greens", - name: "Collard Greens", - sci: "Brassica oleracea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 50-80°F.", - img: "", - }, - { - slug: "mustard-greens", - name: "Mustard Greens", - sci: "Brassica juncea", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps 50-75°F.", - img: "", - }, - { - slug: "horseradish", - name: "Horseradish", - sci: "Armoracia rusticana", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps 45-75°F.", - img: "", - }, - { - slug: "wasabi", - name: "Wasabi", - sci: "Wasabia japonica", - fam: "Brassicaceae", - cat: "herb", - care: "Partial to full shade, constant moisture, cool temps 45-65°F.", - img: "", - }, - - // ── Fabaceae (Legume family) ────────────────────────────────────────── - { - slug: "green-bean", - name: "Green Bean", - sci: "Phaseolus vulgaris", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "soybean", - name: "Soybean", - sci: "Glycine max", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 60-85°F.", - img: "", - }, - { - slug: "peanut", - name: "Peanut", - sci: "Arachis hypogaea", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "chickpea", - name: "Chickpea", - sci: "Cicer arietinum", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm temps 65-85°F.", - img: "", - }, - { - slug: "lentil", - name: "Lentil", - sci: "Lens culinaris", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, cool temps 50-80°F.", - img: "", - }, - { - slug: "faba-bean", - name: "Faba Bean", - sci: "Vicia faba", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 55-70°F.", - img: "", - }, - { - slug: "cowpea", - name: "Cowpea", - sci: "Vigna unguiculata", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm temps 65-95°F.", - img: "", - }, - { - slug: "pigeon-pea", - name: "Pigeon Pea", - sci: "Cajanus cajan", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm tropical temps.", - img: "", - }, - { - slug: "alfalfa", - name: "Alfalfa", - sci: "Medicago sativa", - fam: "Fabaceae", - cat: "herb", - care: "Full sun, drought tolerant, well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "clover", - name: "Clover", - sci: "Trifolium repens", - fam: "Fabaceae", - cat: "herb", - care: "Full sun to partial shade, moderate watering, cool temps.", - img: "", - }, - { - slug: "peas", - name: "Garden Pea", - sci: "Pisum sativum", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool temps 55-70°F.", - img: "", - }, - { - slug: "lupine", - name: "Lupine", - sci: "Lupinus polyphyllus", - fam: "Fabaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained acidic soil.", - img: "", - }, - { - slug: "wisteria", - name: "Wisteria", - sci: "Wisteria sinensis", - fam: "Fabaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil, strong trellis support.", - img: "", - }, - { - slug: "robinia", - name: "Black Locust", - sci: "Robinia pseudoacacia", - fam: "Fabaceae", - cat: "tree", - care: "Full sun, drought tolerant, adaptable to various soils.", - img: "", - }, - - // ── Poaceae (Grass family) ──────────────────────────────────────────── - { - slug: "corn", - name: "Corn (Maize)", - sci: "Zea mays", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "wheat", - name: "Wheat", - sci: "Triticum aestivum", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool to warm temps 55-75°F.", - img: "", - }, - { - slug: "rice", - name: "Rice", - sci: "Oryza sativa", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, flooded field conditions, warm temps 70-95°F.", - img: "", - }, - { - slug: "barley", - name: "Barley", - sci: "Hordeum vulgare", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool temps 55-75°F.", - img: "", - }, - { - slug: "oats", - name: "Oats", - sci: "Avena sativa", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool temps 50-70°F.", - img: "", - }, - { - slug: "sorghum", - name: "Sorghum", - sci: "Sorghum bicolor", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm temps 75-95°F.", - img: "", - }, - { - slug: "sugarcane", - name: "Sugarcane", - sci: "Saccharum officinarum", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, heavy watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "bamboo", - name: "Bamboo", - sci: "Bambusoideae spp.", - fam: "Poaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture, warm temps.", - img: "", - }, - { - slug: "turfgrass", - name: "Turfgrass (Lawn)", - sci: "Festuca/Poa/Lolium spp.", - fam: "Poaceae", - cat: "flower", - care: "Full sun to shade depending on species, consistent watering.", - img: "", - }, - { - slug: "millet", - name: "Millet", - sci: "Pennisetum glaucum", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm temps 75-95°F.", - img: "", - }, - { - slug: "rye", - name: "Rye", - sci: "Secale cereale", - fam: "Poaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool temps 50-70°F.", - img: "", - }, - - // ── Asteraceae (Sunflower family) ───────────────────────────────────── - { - slug: "sunflower", - name: "Sunflower", - sci: "Helianthus annuus", - fam: "Asteraceae", - cat: "flower", - care: "Full sun (6-8h+), moderate watering, warm temps 70-78°F.", - img: "", - }, - { - slug: "lettuce", - name: "Lettuce", - sci: "Lactuca sativa", - fam: "Asteraceae", - cat: "vegetable", - care: "Partial shade to full sun, consistent moisture, cool temps 55-75°F.", - img: "", - }, - { - slug: "artichoke", - name: "Artichoke", - sci: "Cynara cardunculus", - fam: "Asteraceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool summers, well-drained soil.", - img: "", - }, - { - slug: "chicory", - name: "Chicory", - sci: "Cichorium intybus", - fam: "Asteraceae", - cat: "vegetable", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "endive", - name: "Endive", - sci: "Cichorium endivia", - fam: "Asteraceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps.", - img: "", - }, - { - slug: "daisy", - name: "Shasta Daisy", - sci: "Leucanthemum × superbum", - fam: "Asteraceae", - cat: "flower", - care: "Full sun (6h+), moderate watering, well-drained soil.", - img: "", - }, - { - slug: "marigold", - name: "Marigold", - sci: "Tagetes erecta", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "zinnia", - name: "Zinnia", - sci: "Zinnia elegans", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "chrysanthemum", - name: "Chrysanthemum", - sci: "Chrysanthemum morifolium", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, consistent moisture, well-drained soil.", - img: "", - }, - { - slug: "dahlia", - name: "Dahlia", - sci: "Dahlia pinnata", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, consistent watering, well-drained fertile soil.", - img: "", - }, - { - slug: "calendula", - name: "Calendula", - sci: "Calendula officinalis", - fam: "Asteraceae", - cat: "flower", - care: "Full sun to partial shade, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "echinacea", - name: "Coneflower", - sci: "Echinacea purpurea", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, drought tolerant once established.", - img: "", - }, - { - slug: "yarrow", - name: "Yarrow", - sci: "Achillea millefolium", - fam: "Asteraceae", - cat: "flower", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "tarragon", - name: "Tarragon", - sci: "Artemisia dracunculus", - fam: "Asteraceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "stevia", - name: "Stevia", - sci: "Stevia rebaudiana", - fam: "Asteraceae", - cat: "herb", - care: "Full sun to partial shade, consistent moisture, well-drained soil.", - img: "", - }, - - // ── Lamiaceae (Mint family) ─────────────────────────────────────────── - { - slug: "basil", - name: "Basil", - sci: "Ocimum basilicum", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun (6-8h), moderate watering, warm temps 70-90°F.", - img: "", - }, - { - slug: "mint", - name: "Mint", - sci: "Mentha spp.", - fam: "Lamiaceae", - cat: "herb", - care: "Partial shade to full sun, keep soil moist, cool to warm temps.", - img: "", - }, - { - slug: "lavender", - name: "Lavender", - sci: "Lavandula angustifolia", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun, drought tolerant, well-drained alkaline soil pH 6.5-7.5.", - img: "", - }, - { - slug: "rosemary", - name: "Rosemary", - sci: "Salvia rosmarinus", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil, drought tolerant.", - img: "", - }, - { - slug: "thyme", - name: "Thyme", - sci: "Thymus vulgaris", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "oregano", - name: "Oregano", - sci: "Origanum vulgare", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "sage", - name: "Sage", - sci: "Salvia officinalis", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "lemon-balm", - name: "Lemon Balm", - sci: "Melissa officinalis", - fam: "Lamiaceae", - cat: "herb", - care: "Partial shade to sun, consistent moisture.", - img: "", - }, - { - slug: "catnip", - name: "Catnip", - sci: "Nepeta cataria", - fam: "Lamiaceae", - cat: "herb", - care: "Full sun to partial shade, moderate watering.", - img: "", - }, - { - slug: "coleus", - name: "Coleus", - sci: "Coleus scutellarioides", - fam: "Lamiaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, warm temps.", - img: "", - }, - - // ── Apiaceae (Carrot family) ────────────────────────────────────────── - { - slug: "carrot", - name: "Carrot", - sci: "Daucus carota", - fam: "Apiaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps, loose sandy soil.", - img: "", - }, - { - slug: "celery", - name: "Celery", - sci: "Apium graveolens", - fam: "Apiaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps 55-70°F.", - img: "", - }, - { - slug: "parsley", - name: "Parsley", - sci: "Petroselinum crispum", - fam: "Apiaceae", - cat: "herb", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "cilantro", - name: "Cilantro (Coriander)", - sci: "Coriandrum sativum", - fam: "Apiaceae", - cat: "herb", - care: "Partial shade to full sun, consistent moisture, cool temps.", - img: "", - }, - { - slug: "dill", - name: "Dill", - sci: "Anethum graveolens", - fam: "Apiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "fennel", - name: "Fennel", - sci: "Foeniculum vulgare", - fam: "Apiaceae", - cat: "herb", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "parsnip", - name: "Parsnip", - sci: "Pastinaca sativa", - fam: "Apiaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps, loose deep soil.", - img: "", - }, - { - slug: "cumin", - name: "Cumin", - sci: "Cuminum cyminum", - fam: "Apiaceae", - cat: "herb", - care: "Full sun, moderate watering, warm temps 75-85°F.", - img: "", - }, - - // ── Amaryllidaceae (Onion family) ───────────────────────────────────── - { - slug: "onion", - name: "Onion", - sci: "Allium cepa", - fam: "Amaryllidaceae", - cat: "vegetable", - care: "Full sun, consistent watering, cool to warm temps 55-75°F.", - img: "", - }, - { - slug: "garlic", - name: "Garlic", - sci: "Allium sativum", - fam: "Amaryllidaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool temps 55-75°F.", - img: "", - }, - { - slug: "leek", - name: "Leek", - sci: "Allium porrum", - fam: "Amaryllidaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps 55-70°F.", - img: "", - }, - { - slug: "shallot", - name: "Shallot", - sci: "Allium cepa", - fam: "Amaryllidaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps.", - img: "", - }, - { - slug: "chive", - name: "Chive", - sci: "Allium schoenoprasum", - fam: "Amaryllidaceae", - cat: "herb", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - - // ── Araceae (Arum family - houseplants) ────────────────────────────── - { - slug: "monstera", - name: "Monstera", - sci: "Monstera deliciosa", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, water when top 2-3 inches dry, humidity 60-80%.", - img: "", - }, - { - slug: "pothos", - name: "Pothos", - sci: "Epipremnum aureum", - fam: "Araceae", - cat: "houseplant", - care: "Low to bright indirect light, water when top inch dry.", - img: "", - }, - { - slug: "peace-lily", - name: "Peace Lily", - sci: "Spathiphyllum wallisii", - fam: "Araceae", - cat: "houseplant", - care: "Low to medium indirect light, keep soil moist.", - img: "", - }, - { - slug: "philodendron", - name: "Philodendron", - sci: "Philodendron hederaceum", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, water when top inch dry.", - img: "", - }, - { - slug: "anthurium", - name: "Anthurium", - sci: "Anthurium andraeanum", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, high humidity.", - img: "", - }, - { - slug: "alocasia", - name: "Alocasia", - sci: "Alocasia amazonica", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, keep soil moist, high humidity 60-80%.", - img: "", - }, - { - slug: "caladium", - name: "Caladium", - sci: "Caladium bicolor", - fam: "Araceae", - cat: "houseplant", - care: "Partial to full shade, consistent moisture, high humidity.", - img: "", - }, - { - slug: "aglaonema", - name: "Chinese Evergreen", - sci: "Aglaonema commutatum", - fam: "Araceae", - cat: "houseplant", - care: "Low to bright indirect light, moderate water.", - img: "", - }, - { - slug: "dieffenbachia", - name: "Dumb Cane", - sci: "Dieffenbachia seguine", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, warm temps.", - img: "", - }, - { - slug: "spathiphyllum", - name: "Spathiphyllum", - sci: "Spathiphyllum spp.", - fam: "Araceae", - cat: "houseplant", - care: "Low to medium light, keep evenly moist.", - img: "", - }, - - // ── Asparagaceae (Asparagus family) ────────────────────────────────── - { - slug: "asparagus", - name: "Asparagus", - sci: "Asparagus officinalis", - fam: "Asparagaceae", - cat: "vegetable", - care: "Full sun, consistent watering, well-drained sandy soil pH 6.5-7.5.", - img: "", - }, - { - slug: "snake-plant", - name: "Snake Plant", - sci: "Dracaena trifasciata", - fam: "Asparagaceae", - cat: "houseplant", - care: "Tolerates low to bright indirect light, water sparingly.", - img: "", - }, - { - slug: "yucca", - name: "Yucca", - sci: "Yucca gloriosa", - fam: "Asparagaceae", - cat: "houseplant", - care: "Bright light, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "dracaena", - name: "Dracaena", - sci: "Dracaena fragrans", - fam: "Asparagaceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, avoid fluoride.", - img: "", - }, - { - slug: "lily-of-the-valley", - name: "Lily of the Valley", - sci: "Convallaria majalis", - fam: "Asparagaceae", - cat: "flower", - care: "Partial to full shade, consistent moisture, cool temps.", - img: "", - }, - { - slug: "hosta", - name: "Hosta", - sci: "Hosta plantaginea", - fam: "Asparagaceae", - cat: "flower", - care: "Partial to full shade, consistent moisture.", - img: "", - }, - - // ── Orchidaceae (Orchid family) ────────────────────────────────────── - { - slug: "orchid-phalaenopsis", - name: "Phalaenopsis Orchid", - sci: "Phalaenopsis amabilis", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright indirect light, water weekly, bark mix, humidity 50-70%.", - img: "", - }, - { - slug: "orchid-cattleya", - name: "Cattleya Orchid", - sci: "Cattleya labiata", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright light, allow to dry between waterings, high humidity.", - img: "", - }, - { - slug: "orchid-dendrobium", - name: "Dendrobium Orchid", - sci: "Dendrobium nobile", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright light, moderate water, cool winter rest period.", - img: "", - }, - { - slug: "orchid-oncidium", - name: "Oncidium Orchid", - sci: "Oncidium altissimum", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, intermediate temps.", - img: "", - }, - { - slug: "vanilla", - name: "Vanilla Orchid", - sci: "Vanilla planifolia", - fam: "Orchidaceae", - cat: "herb", - care: "Partial shade, consistent moisture, warm humid tropics.", - img: "", - }, - - // ── Cactaceae (Cactus family) ───────────────────────────────────────── - { - slug: "prickly-pear", - name: "Prickly Pear Cactus", - sci: "Opuntia ficus-indica", - fam: "Cactaceae", - cat: "succulent", - care: "Full sun (8h+), water sparingly, extremely well-draining soil.", - img: "", - }, - { - slug: "barrel-cactus", - name: "Barrel Cactus", - sci: "Echinocactus grusonii", - fam: "Cactaceae", - cat: "succulent", - care: "Full sun, minimal water, well-draining cactus mix.", - img: "", - }, - { - slug: "christmas-cactus", - name: "Christmas Cactus", - sci: "Schlumbergera truncata", - fam: "Cactaceae", - cat: "succulent", - care: "Bright indirect light, moderate water, short days for blooms.", - img: "", - }, - { - slug: "saguaro", - name: "Saguaro", - sci: "Carnegiea gigantea", - fam: "Cactaceae", - cat: "succulent", - care: "Full sun, very drought tolerant, well-draining soil.", - img: "", - }, - { - slug: "aloe-vera", - name: "Aloe Vera", - sci: "Aloe barbadensis", - fam: "Asphodelaceae", - cat: "succulent", - care: "Bright indirect to direct light, water every 2-3 weeks.", - img: "", - }, - { - slug: "agave", - name: "Agave", - sci: "Agave americana", - fam: "Asparagaceae", - cat: "succulent", - care: "Full sun, very drought tolerant, well-draining soil.", - img: "", - }, - { - slug: "echeveria", - name: "Echeveria", - sci: "Echeveria elegans", - fam: "Crassulaceae", - cat: "succulent", - care: "Bright direct light, water when soil is completely dry.", - img: "", - }, - { - slug: "jade-plant", - name: "Jade Plant", - sci: "Crassula ovata", - fam: "Crassulaceae", - cat: "succulent", - care: "Bright light, water sparingly, well-draining soil.", - img: "", - }, - { - slug: "sedum", - name: "Sedum (Stonecrop)", - sci: "Sedum acre", - fam: "Crassulaceae", - cat: "succulent", - care: "Full sun to partial shade, drought tolerant.", - img: "", - }, - { - slug: "haworthia", - name: "Haworthia", - sci: "Haworthia fasciata", - fam: "Asphodelaceae", - cat: "succulent", - care: "Bright indirect light, water sparingly, well-draining mix.", - img: "", - }, - - // ── Euphorbiaceae (Spurge family) ───────────────────────────────────── - { - slug: "poinsettia", - name: "Poinsettia", - sci: "Euphorbia pulcherrima", - fam: "Euphorbiaceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, warm temps.", - img: "", - }, - { - slug: "cassava", - name: "Cassava", - sci: "Manihot esculenta", - fam: "Euphorbiaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm tropical temps.", - img: "", - }, - { - slug: "castor-bean", - name: "Castor Bean", - sci: "Ricinus communis", - fam: "Euphorbiaceae", - cat: "flower", - care: "Full sun, consistent moisture, warm temps.", - img: "", - }, - { - slug: "crown-of-thorns", - name: "Crown of Thorns", - sci: "Euphorbia milii", - fam: "Euphorbiaceae", - cat: "succulent", - care: "Full sun, moderate water, well-drained soil.", - img: "", - }, - - // ── Rutaceae (Citrus family) ────────────────────────────────────────── - { - slug: "orange", - name: "Orange", - sci: "Citrus × sinensis", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "lemon", - name: "Lemon", - sci: "Citrus × limon", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained acidic soil.", - img: "", - }, - { - slug: "lime", - name: "Lime", - sci: "Citrus × aurantiifolia", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained acidic soil.", - img: "", - }, - { - slug: "grapefruit", - name: "Grapefruit", - sci: "Citrus × paradisi", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained acidic soil.", - img: "", - }, - { - slug: "mandarin", - name: "Mandarin (Tangerine)", - sci: "Citrus reticulata", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained acidic soil.", - img: "", - }, - { - slug: "kumquat", - name: "Kumquat", - sci: "Fortunella margarita", - fam: "Rutaceae", - cat: "tree", - care: "Full sun, consistent watering, well-drained soil.", - img: "", - }, - - // ── Vitaceae (Grape family) ─────────────────────────────────────────── - { - slug: "grape", - name: "Grape", - sci: "Vitis vinifera", - fam: "Vitaceae", - cat: "fruit", - care: "Full sun, moderate watering, well-drained soil pH 5.5-7.0.", - img: "", - }, - { - slug: "muscadine", - name: "Muscadine", - sci: "Vitis rotundifolia", - fam: "Vitaceae", - cat: "fruit", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - - // ── Musaceae (Banana family) ────────────────────────────────────────── - { - slug: "banana", - name: "Banana", - sci: "Musa acuminata", - fam: "Musaceae", - cat: "fruit", - care: "Full sun, consistent watering, warm temps 75-90°F, rich soil.", - img: "", - }, - { - slug: "plantain", - name: "Plantain", - sci: "Musa × paradisiaca", - fam: "Musaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, warm tropics.", - img: "", - }, - { - slug: "bird-of-paradise", - name: "Bird of Paradise", - sci: "Strelitzia reginae", - fam: "Strelitziaceae", - cat: "flower", - care: "Bright light, consistent moisture, warm temps.", - img: "", - }, - - // ── Lauraceae (Laurel family) ───────────────────────────────────────── - { - slug: "avocado", - name: "Avocado", - sci: "Persea americana", - fam: "Lauraceae", - cat: "tree", - care: "Full sun (6-8h), moderate watering, well-drained soil pH 5.5-7.0.", - img: "", - }, - { - slug: "cinnamon", - name: "Cinnamon", - sci: "Cinnamomum verum", - fam: "Lauraceae", - cat: "tree", - care: "Partial shade, consistent moisture, warm tropics.", - img: "", - }, - { - slug: "bay-laurel", - name: "Bay Laurel", - sci: "Laurus nobilis", - fam: "Lauraceae", - cat: "tree", - care: "Full sun to partial shade, moderate watering.", - img: "", - }, - - // ── Malvaceae (Mallow family) ───────────────────────────────────────── - { - slug: "cocoa", - name: "Cocoa (Cacao)", - sci: "Theobroma cacao", - fam: "Malvaceae", - cat: "tree", - care: "Partial shade, consistent rainfall, warm tropics 65-90°F.", - img: "", - }, - { - slug: "cotton", - name: "Cotton", - sci: "Gossypium hirsutum", - fam: "Malvaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "okra", - name: "Okra", - sci: "Abelmoschus esculentus", - fam: "Malvaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "hibiscus", - name: "Hibiscus", - sci: "Hibiscus rosa-sinensis", - fam: "Malvaceae", - cat: "flower", - care: "Full sun, consistent moisture, warm temps.", - img: "", - }, - { - slug: "hollyhock", - name: "Hollyhock", - sci: "Alcea rosea", - fam: "Malvaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "baobab", - name: "Baobab", - sci: "Adansonia digitata", - fam: "Malvaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "durian", - name: "Durian", - sci: "Durio zibethinus", - fam: "Malvaceae", - cat: "tree", - care: "Partial shade to full sun, consistent rainfall, warm tropics.", - img: "", - }, - - // ── Arecaceae (Palm family) ─────────────────────────────────────────── - { - slug: "coconut", - name: "Coconut Palm", - sci: "Cocos nucifera", - fam: "Arecaceae", - cat: "tree", - care: "Full sun, moderate watering, warm temps 70-95°F.", - img: "", - }, - { - slug: "oil-palm", - name: "Oil Palm", - sci: "Elaeis guineensis", - fam: "Arecaceae", - cat: "tree", - care: "Full sun, consistent moisture, warm tropics 75-95°F.", - img: "", - }, - { - slug: "date-palm", - name: "Date Palm", - sci: "Phoenix dactylifera", - fam: "Arecaceae", - cat: "tree", - care: "Full sun, drought tolerant, warm dry climates.", - img: "", - }, - { - slug: "palm-areca", - name: "Areca Palm", - sci: "Dypsis lutescens", - fam: "Arecaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, high humidity.", - img: "", - }, - { - slug: "palm-parlor", - name: "Parlor Palm", - sci: "Chamaedorea elegans", - fam: "Arecaceae", - cat: "houseplant", - care: "Low to bright indirect light, moderate water.", - img: "", - }, - { - slug: "palm-kentia", - name: "Kentia Palm", - sci: "Howea forsteriana", - fam: "Arecaceae", - cat: "houseplant", - care: "Low to bright indirect light, moderate water.", - img: "", - }, - - // ── Anacardiaceae (Cashew family) ───────────────────────────────────── - { - slug: "mango", - name: "Mango", - sci: "Mangifera indica", - fam: "Anacardiaceae", - cat: "tree", - care: "Full sun, moderate watering, warm temps 70-100°F.", - img: "", - }, - { - slug: "cashew", - name: "Cashew", - sci: "Anacardium occidentale", - fam: "Anacardiaceae", - cat: "tree", - care: "Full sun, moderate watering, warm tropics.", - img: "", - }, - { - slug: "pistachio", - name: "Pistachio", - sci: "Pistacia vera", - fam: "Anacardiaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "poison-ivy", - name: "Poison Ivy", - sci: "Toxicodendron radicans", - fam: "Anacardiaceae", - cat: "flower", - care: "Partial shade to full sun, adaptable to various soils.", - img: "", - }, - - // ── Rubiaceae (Coffee family) ───────────────────────────────────────── - { - slug: "coffee", - name: "Coffee", - sci: "Coffea arabica", - fam: "Rubiaceae", - cat: "tree", - care: "Partial shade, consistent rainfall, moderate temps 60-70°F.", - img: "", - }, - { - slug: "gardenia", - name: "Gardenia", - sci: "Gardenia jasminoides", - fam: "Rubiaceae", - cat: "flower", - care: "Bright indirect light, consistent moisture, acidic soil.", - img: "", - }, - - // ── Theaceae (Tea family) ───────────────────────────────────────────── - { - slug: "tea", - name: "Tea", - sci: "Camellia sinensis", - fam: "Theaceae", - cat: "tree", - care: "Partial shade, consistent moisture, acidic soil pH 4.5-6.0.", - img: "", - }, - { - slug: "camellia", - name: "Camellia", - sci: "Camellia japonica", - fam: "Theaceae", - cat: "flower", - care: "Partial shade, consistent moisture, acidic soil.", - img: "", - }, - - // ── Conifers (Pinaceae, Cupressaceae) ───────────────────────────────── - { - slug: "pine", - name: "Pine", - sci: "Pinus sylvestris", - fam: "Pinaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained acidic soil.", - img: "", - }, - { - slug: "spruce", - name: "Spruce", - sci: "Picea abies", - fam: "Pinaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained acidic soil.", - img: "", - }, - { - slug: "fir", - name: "Fir", - sci: "Abies alba", - fam: "Pinaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "cedar", - name: "Cedar", - sci: "Cedrus libani", - fam: "Pinaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "juniper", - name: "Juniper", - sci: "Juniperus communis", - fam: "Cupressaceae", - cat: "tree", - care: "Full sun, drought tolerant, adaptable to various soils.", - img: "", - }, - { - slug: "cypress", - name: "Cypress", - sci: "Cupressus sempervirens", - fam: "Cupressaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "arborvitae", - name: "Arborvitae", - sci: "Thuja occidentalis", - fam: "Cupressaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - - // ── Fagaceae (Beech family) ─────────────────────────────────────────── - { - slug: "oak", - name: "Oak", - sci: "Quercus robur", - fam: "Fagaceae", - cat: "tree", - care: "Full sun, drought tolerant once established.", - img: "", - }, - { - slug: "beech", - name: "Beech", - sci: "Fagus sylvatica", - fam: "Fagaceae", - cat: "tree", - care: "Partial shade to full sun, moderate moisture.", - img: "", - }, - { - slug: "chestnut", - name: "Chestnut", - sci: "Castanea sativa", - fam: "Fagaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained acidic soil.", - img: "", - }, - - // ── Moraceae (Fig family) ───────────────────────────────────────────── - { - slug: "fiddle-leaf-fig", - name: "Fiddle Leaf Fig", - sci: "Ficus lyrata", - fam: "Moraceae", - cat: "houseplant", - care: "Bright indirect light, water when top 1-2 inches dry, humidity 40-60%.", - img: "", - }, - { - slug: "rubber-tree", - name: "Rubber Tree", - sci: "Ficus elastica", - fam: "Moraceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, warm temps.", - img: "", - }, - { - slug: "weeping-fig", - name: "Weeping Fig", - sci: "Ficus benjamina", - fam: "Moraceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, avoid moving.", - img: "", - }, - { - slug: "fig", - name: "Fig", - sci: "Ficus carica", - fam: "Moraceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "mulberry", - name: "Mulberry", - sci: "Morus alba", - fam: "Moraceae", - cat: "tree", - care: "Full sun, moderate watering, adaptable to various soils.", - img: "", - }, - { - slug: "breadfruit", - name: "Breadfruit", - sci: "Artocarpus altilis", - fam: "Moraceae", - cat: "tree", - care: "Full sun, consistent rainfall, warm tropics.", - img: "", - }, - - // ── Myrtaceae (Myrtle family) ───────────────────────────────────────── - { - slug: "eucalyptus", - name: "Eucalyptus", - sci: "Eucalyptus globulus", - fam: "Myrtaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "guava", - name: "Guava", - sci: "Psidium guajava", - fam: "Myrtaceae", - cat: "tree", - care: "Full sun, consistent watering, warm tropics.", - img: "", - }, - { - slug: "clove", - name: "Clove", - sci: "Syzygium aromaticum", - fam: "Myrtaceae", - cat: "tree", - care: "Partial shade, consistent rainfall, warm humid tropics.", - img: "", - }, - - // ── Bromeliaceae (Bromeliad family) ────────────────────────────────── - { - slug: "pineapple", - name: "Pineapple", - sci: "Ananas comosus", - fam: "Bromeliaceae", - cat: "fruit", - care: "Full sun, moderate watering, warm temps 65-95°F.", - img: "", - }, - { - slug: "bromeliad", - name: "Bromeliad", - sci: "Guzmania spp.", - fam: "Bromeliaceae", - cat: "houseplant", - care: "Bright indirect light, water in central cup, high humidity.", - img: "", - }, - { - slug: "spanish-moss", - name: "Spanish Moss", - sci: "Tillandsia usneoides", - fam: "Bromeliaceae", - cat: "houseplant", - care: "Bright indirect light, mist regularly.", - img: "", - }, - - // ── Convolvulaceae (Morning glory family) ───────────────────────────── - { - slug: "sweet-potato", - name: "Sweet Potato", - sci: "Ipomoea batatas", - fam: "Convolvulaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps 65-95°F.", - img: "", - }, - { - slug: "morning-glory", - name: "Morning Glory", - sci: "Ipomoea purpurea", - fam: "Convolvulaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - - // ── Chenopodiaceae / Amaranthaceae ────────────────────────────────── - { - slug: "spinach", - name: "Spinach", - sci: "Spinacia oleracea", - fam: "Amaranthaceae", - cat: "vegetable", - care: "Partial shade to full sun, consistent moisture, cool temps 50-70°F.", - img: "", - }, - { - slug: "swiss-chard", - name: "Swiss Chard", - sci: "Beta vulgaris", - fam: "Amaranthaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "beet", - name: "Beet", - sci: "Beta vulgaris", - fam: "Amaranthaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps.", - img: "", - }, - { - slug: "quinoa", - name: "Quinoa", - sci: "Chenopodium quinoa", - fam: "Amaranthaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool temps 50-75°F.", - img: "", - }, - { - slug: "amaranth", - name: "Amaranth", - sci: "Amaranthus cruentus", - fam: "Amaranthaceae", - cat: "vegetable", - care: "Full sun, drought tolerant, warm temps.", - img: "", - }, - - // ── Polygonaceae (Buckwheat family) ────────────────────────────────── - { - slug: "rhubarb", - name: "Rhubarb", - sci: "Rheum rhabarbarum", - fam: "Polygonaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps, rich soil.", - img: "", - }, - { - slug: "buckwheat", - name: "Buckwheat", - sci: "Fagopyrum esculentum", - fam: "Polygonaceae", - cat: "vegetable", - care: "Full sun, moderate watering, cool to warm temps.", - img: "", - }, - - // ── Caricaceae (Papaya family) ──────────────────────────────────────── - { - slug: "papaya", - name: "Papaya", - sci: "Carica papaya", - fam: "Caricaceae", - cat: "fruit", - care: "Full sun, consistent watering, warm temps 70-90°F.", - img: "", - }, - - // ── Oleaceae (Olive family) ─────────────────────────────────────────── - { - slug: "olive", - name: "Olive", - sci: "Olea europaea", - fam: "Oleaceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "jasmine", - name: "Jasmine", - sci: "Jasminum officinale", - fam: "Oleaceae", - cat: "flower", - care: "Full sun to partial shade, regular watering, warm temps.", - img: "", - }, - { - slug: "lilac", - name: "Lilac", - sci: "Syringa vulgaris", - fam: "Oleaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained neutral soil.", - img: "", - }, - { - slug: "ash", - name: "Ash", - sci: "Fraxinus excelsior", - fam: "Oleaceae", - cat: "tree", - care: "Full sun, moderate watering, adaptable to various soils.", - img: "", - }, - - // ── Cannabaceae (Hemp family) ───────────────────────────────────────── - { - slug: "hops", - name: "Hops", - sci: "Humulus lupulus", - fam: "Cannabaceae", - cat: "herb", - care: "Full sun, consistent watering, well-drained soil pH 6.0-7.0.", - img: "", - }, - { - slug: "hemp", - name: "Hemp", - sci: "Cannabis sativa", - fam: "Cannabaceae", - cat: "herb", - care: "Full sun, consistent watering, well-drained fertile soil.", - img: "", - }, - - // ── Additional houseplants ──────────────────────────────────────────── - { - slug: "fern-boston", - name: "Boston Fern", - sci: "Nephrolepis exaltata", - fam: "Nephrolepidaceae", - cat: "houseplant", - care: "Bright indirect light, keep soil moist, high humidity.", - img: "", - }, - { - slug: "fern-maidenhair", - name: "Maidenhair Fern", - sci: "Adiantum capillus-veneris", - fam: "Pteridaceae", - cat: "houseplant", - care: "Partial shade, constant moisture, high humidity.", - img: "", - }, - { - slug: "spider-plant", - name: "Spider Plant", - sci: "Chlorophytum comosum", - fam: "Asparagaceae", - cat: "houseplant", - care: "Bright indirect light to low light, moderate water.", - img: "", - }, - { - slug: "zz-plant", - name: "ZZ Plant", - sci: "Zamioculcas zamiifolia", - fam: "Araceae", - cat: "houseplant", - care: "Low to bright indirect light, drought tolerant, water sparingly.", - img: "", - }, - { - slug: "prayer-plant", - name: "Prayer Plant", - sci: "Maranta leuconeura", - fam: "Marantaceae", - cat: "houseplant", - care: "Low to medium indirect light, consistent moisture.", - img: "", - }, - { - slug: "calathea", - name: "Calathea", - sci: "Calathea orbifolia", - fam: "Marantaceae", - cat: "houseplant", - care: "Low to medium indirect light, consistent moisture, high humidity.", - img: "", - }, - { - slug: "pilea", - name: "Pilea (Chinese Money Plant)", - sci: "Pilea peperomioides", - fam: "Urticaceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, well-drained soil.", - img: "", - }, - { - slug: "tradescantia", - name: "Wandering Jew", - sci: "Tradescantia zebrina", - fam: "Commelinaceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, easy to propagate.", - img: "", - }, - { - slug: "succulent-echeveria", - name: "Echeveria", - sci: "Echeveria elegans", - fam: "Crassulaceae", - cat: "succulent", - care: "Bright direct light, water when dry, excellent drainage.", - img: "", - }, - { - slug: "money-tree", - name: "Money Tree", - sci: "Pachira aquatica", - fam: "Malvaceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, warm temps.", - img: "", - }, - { - slug: "palm-cat", - name: "Cat Palm", - sci: "Chamaedorea cataractarum", - fam: "Arecaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, humidity.", - img: "", - }, - { - slug: "ficus-altissima", - name: "Ficus Altissima", - sci: "Ficus altissima", - fam: "Moraceae", - cat: "houseplant", - care: "Bright indirect light, moderate water, warm temps.", - img: "", - }, - { - slug: "string-of-pearls", - name: "String of Pearls", - sci: "Curio rowleyanus", - fam: "Asteraceae", - cat: "succulent", - care: "Bright light, water sparingly, well-draining soil.", - img: "", - }, - { - slug: "burros-tail", - name: "Burro's Tail", - sci: "Sedum morganianum", - fam: "Crassulaceae", - cat: "succulent", - care: "Bright light, water sparingly, well-draining hanging basket.", - img: "", - }, - { - slug: "snake-plant-masoniana", - name: "Whale Fin Snake Plant", - sci: "Dracaena masoniana", - fam: "Asparagaceae", - cat: "houseplant", - care: "Low to bright light, water sparingly.", - img: "", - }, - - // ── Additional tropical fruits ──────────────────────────────────────── - { - slug: "passion-fruit", - name: "Passion Fruit", - sci: "Passiflora edulis", - fam: "Passifloraceae", - cat: "fruit", - care: "Full sun, consistent watering, warm temps 70-85°F.", - img: "", - }, - { - slug: "kiwi", - name: "Kiwi", - sci: "Actinidia deliciosa", - fam: "Actinidiaceae", - cat: "fruit", - care: "Full sun, consistent watering, well-drained soil.", - img: "", - }, - { - slug: "lychee", - name: "Lychee", - sci: "Litchi chinensis", - fam: "Sapindaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture, warm tropics.", - img: "", - }, - { - slug: "rambutan", - name: "Rambutan", - sci: "Nephelium lappaceum", - fam: "Sapindaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture, warm tropics.", - img: "", - }, - { - slug: "jackfruit", - name: "Jackfruit", - sci: "Artocarpus heterophyllus", - fam: "Moraceae", - cat: "tree", - care: "Full sun, consistent watering, warm tropics.", - img: "", - }, - { - slug: "dragon-fruit", - name: "Dragon Fruit", - sci: "Hylocereus undatus", - fam: "Cactaceae", - cat: "fruit", - care: "Full sun, moderate watering, well-draining soil.", - img: "", - }, - { - slug: "pomegranate", - name: "Pomegranate", - sci: "Punica granatum", - fam: "Lythraceae", - cat: "tree", - care: "Full sun, drought tolerant, well-drained soil.", - img: "", - }, - { - slug: "persimmon", - name: "Persimmon", - sci: "Diospyros kaki", - fam: "Ebenaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - - // ── Additional flowers & ornamentals ────────────────────────────────── - { - slug: "tulip", - name: "Tulip", - sci: "Tulipa gesneriana", - fam: "Liliaceae", - cat: "flower", - care: "Full sun to partial shade, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "daffodil", - name: "Daffodil", - sci: "Narcissus pseudonarcissus", - fam: "Amaryllidaceae", - cat: "flower", - care: "Full sun to partial shade, moderate watering.", - img: "", - }, - { - slug: "iris", - name: "Iris", - sci: "Iris germanica", - fam: "Iridaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "lily", - name: "Lily", - sci: "Lilium candidum", - fam: "Liliaceae", - cat: "flower", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "peony", - name: "Peony", - sci: "Paeonia lactiflora", - fam: "Paeoniaceae", - cat: "flower", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "hydrangea", - name: "Hydrangea", - sci: "Hydrangea macrophylla", - fam: "Hydrangeaceae", - cat: "flower", - care: "Partial shade, consistent moisture, acidic soil for blue blooms.", - img: "", - }, - { - slug: "rhododendron", - name: "Rhododendron", - sci: "Rhododendron ponticum", - fam: "Ericaceae", - cat: "flower", - care: "Partial shade, consistent moisture, acidic soil.", - img: "", - }, - { - slug: "azalea", - name: "Azalea", - sci: "Rhododendron simsii", - fam: "Ericaceae", - cat: "flower", - care: "Partial shade, acidic soil, consistent moisture.", - img: "", - }, - { - slug: "magnolia", - name: "Magnolia", - sci: "Magnolia grandiflora", - fam: "Magnoliaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "dogwood", - name: "Dogwood", - sci: "Cornus florida", - fam: "Cornaceae", - cat: "tree", - care: "Partial shade, consistent moisture, acidic soil.", - img: "", - }, - { - slug: "maple", - name: "Maple", - sci: "Acer saccharum", - fam: "Sapindaceae", - cat: "tree", - care: "Full sun to partial shade, moderate watering.", - img: "", - }, - { - slug: "birch", - name: "Birch", - sci: "Betula pendula", - fam: "Betulaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "elm", - name: "Elm", - sci: "Ulmus americana", - fam: "Ulmaceae", - cat: "tree", - care: "Full sun, moderate watering, adaptable.", - img: "", - }, - { - slug: "willow", - name: "Willow", - sci: "Salix babylonica", - fam: "Salicaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "poplar", - name: "Poplar", - sci: "Populus nigra", - fam: "Salicaceae", - cat: "tree", - care: "Full sun, consistent moisture.", - img: "", - }, - { - slug: "sycamore", - name: "Sycamore", - sci: "Platanus occidentalis", - fam: "Platanaceae", - cat: "tree", - care: "Full sun, moderate watering, adaptable.", - img: "", - }, - { - slug: "hickory", - name: "Hickory", - sci: "Carya ovata", - fam: "Juglandaceae", - cat: "tree", - care: "Full sun, moderate watering.", - img: "", - }, - { - slug: "pecan", - name: "Pecan", - sci: "Carya illinoinensis", - fam: "Juglandaceae", - cat: "tree", - care: "Full sun, consistent watering, deep soil.", - img: "", - }, - { - slug: "walnut", - name: "Walnut", - sci: "Juglans regia", - fam: "Juglandaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - // ── Additional plants to reach 9,300+ diseases ───────────── - { - slug: "lilac", - name: "Lilac", - sci: "Syringa vulgaris", - fam: "Oleaceae", - cat: "flower", - care: "Full sun, moderate watering, neutral well-drained soil.", - img: "", - }, - { - slug: "fern-staghorn", - name: "Staghorn Fern", - sci: "Platycerium bifurcatum", - fam: "Polypodiaceae", - cat: "houseplant", - care: "Bright indirect light, mist regularly, epiphytic mount.", - img: "", - }, - { - slug: "fern-birds-nest", - name: "Bird's Nest Fern", - sci: "Asplenium nidus", - fam: "Aspleniaceae", - cat: "houseplant", - care: "Low to medium indirect light, consistent moisture, humidity.", - img: "", - }, - { - slug: "philodendron-brasil", - name: "Philodendron Brasil", - sci: "Philodendron hederaceum", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, water when top inch dry.", - img: "", - }, - { - slug: "philodendron-monstera", - name: "Monstera Adansonii", - sci: "Monstera adansonii", - fam: "Araceae", - cat: "houseplant", - care: "Bright indirect light, water when top 2 inches dry.", - img: "", - }, - { - slug: "pothos-marble-queen", - name: "Marble Queen Pothos", - sci: "Epipremnum aureum", - fam: "Araceae", - cat: "houseplant", - care: "Low to bright indirect light, water when top inch dry.", - img: "", - }, - { - slug: "peace-lily-sensation", - name: "Sensation Peace Lily", - sci: "Spathiphyllum 'Sensation'", - fam: "Araceae", - cat: "houseplant", - care: "Low to medium indirect light, keep soil moist.", - img: "", - }, - { - slug: "phalaenopsis-orchid", - name: "Moth Orchid", - sci: "Phalaenopsis amabilis", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright indirect light, water weekly, bark mix.", - img: "", - }, - { - slug: "cattleya-orchid", - name: "Cattleya Orchid", - sci: "Cattleya labiata", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright light, dry between waterings, high humidity.", - img: "", - }, - { - slug: "dendrobium-orchid", - name: "Dendrobium Orchid", - sci: "Dendrobium nobile", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright light, moderate water, cool rest period.", - img: "", - }, - { - slug: "oncidium-orchid", - name: "Oncidium Orchid", - sci: "Oncidium altissimum", - fam: "Orchidaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture.", - img: "", - }, - { - slug: "begonia", - name: "Begonia", - sci: "Begonia semperflorens", - fam: "Begoniaceae", - cat: "flower", - care: "Partial shade, consistent moisture, well-drained soil.", - img: "", - }, - { - slug: "impatiens", - name: "Impatiens", - sci: "Impatiens walleriana", - fam: "Balsaminaceae", - cat: "flower", - care: "Partial to full shade, consistent moisture.", - img: "", - }, - { - slug: "geranium", - name: "Geranium", - sci: "Pelargonium × hortorum", - fam: "Geraniaceae", - cat: "flower", - care: "Full sun to partial shade, moderate watering.", - img: "", - }, - { - slug: "cyclamen", - name: "Cyclamen", - sci: "Cyclamen persicum", - fam: "Primulaceae", - cat: "flower", - care: "Bright indirect light, cool temps 55-65°F.", - img: "", - }, - { - slug: "african-violet", - name: "African Violet", - sci: "Saintpaulia ionantha", - fam: "Gesneriaceae", - cat: "houseplant", - care: "Bright indirect light, warm temps, avoid leaf wetting.", - img: "", - }, - { - slug: "gloxinia", - name: "Gloxinia", - sci: "Sinningia speciosa", - fam: "Gesneriaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, high humidity.", - img: "", - }, - { - slug: "cucumber-horned", - name: "Horned Melon (Kiwano)", - sci: "Cucumis metuliferus", - fam: "Cucurbitaceae", - cat: "vegetable", - care: "Full sun, warm temps, well-drained soil.", - img: "", - }, - { - slug: "sweet-potato-leaf", - name: "Sweet Potato Vine (Ornamental)", - sci: "Ipomoea batatas", - fam: "Convolvulaceae", - cat: "houseplant", - care: "Bright indirect to full sun, moderate water.", - img: "", - }, - { - slug: "ivy-english", - name: "English Ivy", - sci: "Hedera helix", - fam: "Araliaceae", - cat: "houseplant", - care: "Low to bright indirect light, moderate water.", - img: "", - }, - { - slug: "ivy-swedish", - name: "Swedish Ivy", - sci: "Plectranthus verticillatus", - fam: "Lamiaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture.", - img: "", - }, - { - slug: "banana-dwarf", - name: "Dwarf Banana", - sci: "Musa acuminata 'Dwarf Cavendish'", - fam: "Musaceae", - cat: "houseplant", - care: "Bright indirect to full sun, consistent moisture.", - img: "", - }, - { - slug: "mimosa", - name: "Mimosa (Silk Tree)", - sci: "Albizia julibrissin", - fam: "Fabaceae", - cat: "tree", - care: "Full sun, drought tolerant, adaptable.", - img: "", - }, - { - slug: "kentucky-coffee", - name: "Kentucky Coffee Tree", - sci: "Gymnocladus dioicus", - fam: "Fabaceae", - cat: "tree", - care: "Full sun, adaptable, drought tolerant.", - img: "", - }, - { - slug: "redbud", - name: "Redbud", - sci: "Cercis canadensis", - fam: "Fabaceae", - cat: "tree", - care: "Partial shade to full sun, moderate watering.", - img: "", - }, - { - slug: "tulip-tree", - name: "Tulip Tree", - sci: "Liriodendron tulipifera", - fam: "Magnoliaceae", - cat: "tree", - care: "Full sun, consistent moisture, deep soil.", - img: "", - }, - { - slug: "sweetgum", - name: "Sweetgum", - sci: "Liquidambar styraciflua", - fam: "Altingiaceae", - cat: "tree", - care: "Full sun, moderate watering, adaptable.", - img: "", - }, - { - slug: "crabapple", - name: "Crabapple", - sci: "Malus sylvestris", - fam: "Rosaceae", - cat: "tree", - care: "Full sun, moderate watering, well-drained soil.", - img: "", - }, - { - slug: "serviceberry", - name: "Serviceberry", - sci: "Amelanchier canadensis", - fam: "Rosaceae", - cat: "tree", - care: "Partial shade to full sun, consistent moisture.", - img: "", - }, - { - slug: "chokecherry", - name: "Chokecherry", - sci: "Prunus virginiana", - fam: "Rosaceae", - cat: "tree", - care: "Full sun to partial shade, adaptable.", - img: "", - }, - { - slug: "buckeye", - name: "Buckeye (Ohio)", - sci: "Aesculus glabra", - fam: "Sapindaceae", - cat: "tree", - care: "Partial shade to full sun, consistent moisture.", - img: "", - }, - { - slug: "linden", - name: "Linden (Basswood)", - sci: "Tilia americana", - fam: "Malvaceae", - cat: "tree", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "ginkgo", - name: "Ginkgo", - sci: "Ginkgo biloba", - fam: "Ginkgoaceae", - cat: "tree", - care: "Full sun, adaptable, very tolerant.", - img: "", - }, - { - slug: "ficus-microcarpa", - name: "Ficus Microcarpa (Ginseng Ficus)", - sci: "Ficus microcarpa", - fam: "Moraceae", - cat: "houseplant", - care: "Bright indirect light, moderate water.", - img: "", - }, - { - slug: "schefflera", - name: "Schefflera (Umbrella Tree)", - sci: "Schefflera arboricola", - fam: "Araliaceae", - cat: "houseplant", - care: "Bright indirect to low light, moderate water.", - img: "", - }, - { - slug: "maranta", - name: "Red Prayer Plant", - sci: "Maranta leuconeura var. erythroneura", - fam: "Marantaceae", - cat: "houseplant", - care: "Low to medium indirect light, consistent moisture.", - img: "", - }, - { - slug: "stromanthe", - name: "Stromanthe Triostar", - sci: "Stromanthe sanguinea", - fam: "Marantaceae", - cat: "houseplant", - care: "Bright indirect light, consistent moisture, high humidity.", - img: "", - }, - { - slug: "bok-choy-shanghai", - name: "Shanghai Bok Choy", - sci: "Brassica rapa var. chinensis", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps.", - img: "", - }, - { - slug: "tatsoi", - name: "Tatsoi", - sci: "Brassica rapa var. narinosa", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture, cool temps.", - img: "", - }, - { - slug: "mizuna", - name: "Mizuna", - sci: "Brassica rapa var. japonica", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun to partial shade, consistent moisture.", - img: "", - }, - { - slug: "kohlrabi", - name: "Kohlrabi", - sci: "Brassica oleracea var. gongylodes", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps.", - img: "", - }, - { - slug: "rapini", - name: "Rapini (Broccoli Rabe)", - sci: "Brassica rapa var. ruvo", - fam: "Brassicaceae", - cat: "vegetable", - care: "Full sun, consistent moisture, cool temps.", - img: "", - }, - { - slug: "jicama", - name: "Jicama", - sci: "Pachyrhizus erosus", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, consistent watering, warm temps.", - img: "", - }, - { - slug: "adzuki-bean", - name: "Adzuki Bean", - sci: "Vigna angularis", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, moderate watering, warm temps.", - img: "", - }, - { - slug: "mung-bean", - name: "Mung Bean", - sci: "Vigna radiata", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, moderate water, warm temps.", - img: "", - }, - { - slug: "garbanzo", - name: "Garbanzo (Chickpea)", - sci: "Cicer arietinum", - fam: "Fabaceae", - cat: "vegetable", - care: "Full sun, drought tolerant.", - img: "", - }, -]; - -export default PLANTS; diff --git a/scripts/retry-wiki.ts b/scripts/retry-wiki.ts deleted file mode 100644 index 42df03f..0000000 --- a/scripts/retry-wiki.ts +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node -/** - * Retry Wikipedia pages that got rate-limited - * - * Uses longer delays (5s) for pages that previously got 429. - */ -import "dotenv/config"; -import { closeDb } from "../src/lib/db/index"; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; -import { resolve, dirname } from "path"; -import { fileURLToPath } from "url"; - -const __filedir = dirname(fileURLToPath(import.meta.url)); -function cacheGet(k: string): string | null { - const p = resolve(__filedir, ".scraper-cache", encodeURIComponent(k) + ".json"); - return existsSync(p) ? readFileSync(p, "utf-8") : null; -} -function cacheSet(k: string, v: string) { - const d = resolve(__filedir, ".scraper-cache"); - if (!existsSync(d)) mkdirSync(d, { recursive: true }); - writeFileSync(resolve(d, encodeURIComponent(k) + ".json"), v, "utf-8"); -} - -const PAGES_TO_RETRY = [ - "List_of_cranberry_diseases", - "List_of_cucurbit_diseases", - "List_of_grape_diseases", - "List_of_hops_diseases", - "List_of_rice_diseases", - "List_of_rose_diseases", - "List_of_sorghum_diseases", - "List_of_soybean_diseases", - "List_of_spinach_diseases", - "List_of_strawberry_diseases", - "List_of_sugarcane_diseases", - "List_of_sunflower_diseases", - "List_of_sweet_potato_diseases", -]; - -async function fetchWT(page: string): Promise { - const key = `wt-${page}`; - const c = cacheGet(key); - if (c) return c; - const url = `https://en.wikipedia.org/w/api.php?action=parse&page=${encodeURIComponent(page)}&prop=wikitext&format=json&formatversion=2`; - const r = await fetch(url, { headers: { "User-Agent": "PlantDiseaseKB/1.0 (research)" } }); - if (!r.ok) throw new Error(`HTTP ${r.status}`); - const d = (await r.json()) as { parse: { wikitext: string }; error?: { info: string } }; - if (d.error) throw new Error(d.error.info); - cacheSet(key, d.parse.wikitext); - return d.parse.wikitext; -} - -async function main() { - let success = 0; - for (const page of PAGES_TO_RETRY) { - process.stdout.write(`📋 ${page}... `); - try { - await new Promise((r) => setTimeout(r, 5000 + Math.random() * 2000)); - const wt = await fetchWT(page); - console.log(`✅ ${wt.length} bytes`); - success++; - } catch (e) { - console.log(`❌ ${e instanceof Error ? e.message : e}`); - } - } - await new Promise((r) => setTimeout(r, 2000)); - console.log(`\nDone. ${success}/${PAGES_TO_RETRY.length} pages fetched`); - closeDb(); -} - -main().catch(console.error); diff --git a/scripts/scrape-wikipedia.ts b/scripts/scrape-wikipedia.ts deleted file mode 100644 index 05b7ac2..0000000 --- a/scripts/scrape-wikipedia.ts +++ /dev/null @@ -1,1140 +0,0 @@ -#!/usr/bin/env node -/** - * Wikipedia Plant Disease Scraper - * - * Fetches disease data from Wikipedia "List of X diseases" pages via - * the MediaWiki API, parses wikitext tables, and stores in Turso. - * - * Usage: cd apps/web && npx tsx scripts/scrape-wikipedia.ts - */ - -import "dotenv/config"; -import { sql } from "drizzle-orm"; -import { getDb, closeDb } from "../src/lib/db/index"; -import { plants, diseases, scrapeSources } from "../src/lib/db/schema"; -import type { CausalAgentType, Severity } from "../src/lib/types"; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; -import { resolve, dirname } from "path"; -import { fileURLToPath } from "url"; - -// ─── Paths ─────────────────────────────────────────────────────────────────── - -const __filedir = dirname(fileURLToPath(import.meta.url)); - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function slugify(s: string): string { - return s - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .trim() - .replace(/^-|-$/g, ""); -} - -function clean(t: string): string { - return t - .replace(/\[\[[^\]]*?\|([^\]]*)\]\]/g, "$1") - .replace(/\[\[([^\]]*)\]\]/g, "$1") - .replace(/'''?/g, "") - .replace(/''/g, "") - .replace(/]*>.*?<\/ref>/gi, "") - .replace(//gi, " ") - .replace(/&/g, "&") - .replace(/ /g, " ") - .replace(/{{[^}]*}}/g, "") - .replace(/\s{2,}/g, " ") - .trim(); -} - -// ─── Cache ─────────────────────────────────────────────────────────────────── - -function cacheGet(k: string): string | null { - const p = resolve(__filedir, ".scraper-cache", encodeURIComponent(k) + ".json"); - return existsSync(p) ? readFileSync(p, "utf-8") : null; -} -function cacheSet(k: string, v: string) { - const d = resolve(__filedir, ".scraper-cache"); - if (!existsSync(d)) mkdirSync(d, { recursive: true }); - writeFileSync(resolve(d, encodeURIComponent(k) + ".json"), v, "utf-8"); -} - -// ─── Wikipedia API ─────────────────────────────────────────────────────────── - -let lastFetchTime = 0; -const MIN_DELAY_MS = 600; // Wait at least 600ms between requests - -async function fetchWT(page: string): Promise { - const key = `wt-${page}`; - const c = cacheGet(key); - if (c) return c; - - // Rate limiting - const now = Date.now(); - const wait = Math.max(0, MIN_DELAY_MS - (now - lastFetchTime)); - if (wait > 0) await new Promise((r) => setTimeout(r, wait)); - lastFetchTime = Date.now(); - - const url = `https://en.wikipedia.org/w/api.php?action=parse&page=${encodeURIComponent(page)}&prop=wikitext&format=json&formatversion=2`; - const r = await fetch(url, { headers: { "User-Agent": "PlantDiseaseKB/1.0 (research)" } }); - - if (r.status === 429) { - // Rate limited — wait longer and retry once - console.log(` ⏳ Rate limited, waiting 5s...`); - await new Promise((r) => setTimeout(r, 5000)); - const r2 = await fetch(url, { headers: { "User-Agent": "PlantDiseaseKB/1.0 (research)" } }); - if (!r2.ok) throw new Error(`HTTP ${r2.status} for ${page} (after retry)`); - const d2 = (await r2.json()) as { parse: { wikitext: string }; error?: { info: string } }; - if (d2.error) throw new Error(`API error: ${d2.error.info || JSON.stringify(d2.error)}`); - if (!d2.parse) throw new Error(`Page "${page}" not found`); - const wt2 = d2.parse.wikitext; - cacheSet(key, wt2); - return wt2; - } - - if (!r.ok) throw new Error(`HTTP ${r.status} for ${page}`); - const d = (await r.json()) as { parse: { wikitext: string }; error?: { info: string } }; - if (d.error) throw new Error(`API error: ${d.error.info || JSON.stringify(d.error)}`); - if (!d.parse) throw new Error(`Page "${page}" not found`); - const wt: string = d.parse.wikitext; - cacheSet(key, wt); - return wt; -} - -// ─── Section → type ───────────────────────────────────────────────────────── - -const SECTION_RULES: [RegExp, CausalAgentType][] = [ - [/bacteri/i, "bacterial"], - [/phytoplasma/i, "bacterial"], - [/fungus|fungal|fungi/i, "fungal"], - [/oomycete/i, "fungal"], - [/viral|viroid/i, "viral"], - [/nematode/i, "environmental"], - [ - /miscellaneous|disorder|abiotic|nutrient|physiological|insect|pest|lepidoptera|mite|parasitic/i, - "environmental", - ], -]; - -function sectionType(name: string): CausalAgentType | null { - for (const [re, t] of SECTION_RULES) if (re.test(name)) return t; - return null; -} - -// ─── Wikitable parser ──────────────────────────────────────────────────────── - -interface Row { - name: string; - sci: string; -} - -function parseRows(table: string): Row[] { - const out: Row[] = []; - const lines = table.split("\n").map((l) => l.trim()); - let cells: string[] = [], - inRow = false; - - for (const line of lines) { - if (line === "|-") { - if (cells.length) { - const r = mkRow(cells); - if (r) out.push(r); - } - cells = []; - inRow = true; - } else if (inRow && (line.startsWith("|") || line.startsWith("!"))) { - if (line.includes("||")) - cells.push(...line.split("||").map((p) => p.replace(/^[|!]+/, "").trim())); - else cells.push(line.replace(/^[|!]+/, "").trim()); - } else if (inRow && line && !line.startsWith("|") && !line.startsWith("!")) { - if (cells.length) cells[cells.length - 1] += " " + line; - } - } - if (cells.length) { - const r = mkRow(cells); - if (r) out.push(r); - } - return out; -} - -function mkRow(c: string[]): Row | null { - const name = clean(c[0] || ""); - if (!name || /^(Common|Scientific|colspan)/i.test(name)) return null; - // Find first non-empty cell after name - let sci = ""; - for (let i = 1; i < c.length; i++) { - const cl = clean(c[i]); - if (cl && cl.length > 2 && !cl.startsWith("'")) { - sci = cl; - break; - } - } - return { name, sci }; -} - -// ─── Fetch & parse one page ────────────────────────────────────────────────── - -interface TableData { - type: CausalAgentType; - rows: Row[]; -} - -async function scrapePage(page: string): Promise { - const wt = await fetchWT(page); - const tables: TableData[] = []; - - // Strategy 1: section headers with embedded wikitable - const seenKeys = new Set(); - const parts = wt.split(/\n(?===)/); - for (const part of parts) { - const h = part.match(/^==([^=]+)==/); - if (!h) continue; - const type = sectionType(h[1]); - if (!type) continue; - - const tbl = part.match(/\{\|[\s\S]*?\|\}/); - if (!tbl) continue; - - const rows = parseRows(tbl[0]); - if (rows.length) { - const key = type + "|" + rows.map((r) => r.name).join(","); - if (!seenKeys.has(key)) { - seenKeys.add(key); - tables.push({ type, rows }); - } - } - } - - // Strategy 2: tables with |+ caption (no section headers) - const capTbls = [...wt.matchAll(/\{\|[\s\S]*?\|\}/g)]; - for (const m of capTbls) { - const blk = m[0]; - const cap = blk.match(/^\|\+(.+)/m); - if (!cap) continue; - const type = sectionType(cap[1]); - if (!type) continue; - - const rows = parseRows(blk); - if (rows.length) { - const key = type + "|" + rows.map((r) => r.name).join(","); - if (!seenKeys.has(key)) { - seenKeys.add(key); - tables.push({ type, rows }); - } - } - } - - return tables; -} - -// ─── Disease templates (sourced from UW-Madison PDDC factsheets) ─────────── - -const TEMPLATES: Record< - CausalAgentType, - { - symptoms: string[]; - causes: string[]; - treatment: string[]; - prevention: string[]; - severity: Severity; - } -> = { - fungal: { - severity: "moderate", - symptoms: [ - "Leaf spots or lesions with concentric rings or characteristic fungal growth", - "Yellowing and browning of infected plant tissue starting from lower leaves", - "Wilting, stunting, or dieback of infected plants under favorable conditions", - "Premature defoliation in moderate to severe cases", - "Reduced yield, fruit rot, or poor fruit quality on affected plants", - ], - causes: [ - "Fungal pathogens surviving in soil, plant debris, or on infected seed material", - "Warm humid conditions (60-85°F) with extended leaf wetness periods", - "Spores spread by wind, rain splash, insects, or contaminated tools and hands", - "Dense plantings with poor air circulation and frequent overhead irrigation", - ], - treatment: [ - "Remove and destroy all infected plant material — do not compost", - "Apply appropriate fungicide (copper, sulfur, chlorothalonil) as directed on label", - "Improve air circulation through proper plant spacing, pruning, and staking", - "Water at soil level using drip irrigation or soaker hoses to keep foliage dry", - "Apply 2-3 inches of organic mulch to reduce soil splash onto lower leaves", - ], - prevention: [ - "Plant resistant varieties when available", - "Practice 2-3 year crop rotation with non-host plant families", - "Space plants adequately for good air movement", - "Avoid overhead watering; water early in the day", - "Remove and dispose of all plant debris at end of growing season", - ], - }, - bacterial: { - severity: "high", - symptoms: [ - "Water-soaked lesions on leaves, stems, and fruit that turn brown or black", - "Wilting of branches or entire plant despite adequate soil moisture", - "Vascular discoloration visible when stems are cut crosswise near soil line", - "Bacterial ooze or exudate from cut stems or infected tissue in humid weather", - "Cankers on stems with associated gumming or branch dieback", - ], - causes: [ - "Bacterial pathogens entering through wounds, stomata, or other natural openings", - "Spread by rain splash, irrigation water, insects, and contaminated pruning tools", - "Warm humid conditions (75-90°F) favor rapid bacterial multiplication", - "Bacteria survive in infected plant debris, soil, and on seed surfaces between seasons", - ], - treatment: [ - "Remove and destroy infected plants immediately — bag and remove from garden", - "Prune infected branches at least 12 inches below visible symptoms", - "Sterilize all pruning tools with 10% bleach or 70% alcohol between every cut", - "No chemical cure exists once plants are infected; copper may slow early infections", - "Disinfect hands, gloves, and clothing after handling infected plant material", - ], - prevention: [ - "Use certified disease-free seed and pathogen-free transplants", - "Practice long crop rotation (3-5 years) with unrelated crop families", - "Avoid overhead irrigation; use drip irrigation or soaker hoses instead", - "Control insect vectors (cucumber beetles, flea beetles) that spread bacteria", - "Sanitize garden tools, stakes, and cages regularly", - ], - }, - viral: { - severity: "high", - symptoms: [ - "Mottled mosaic pattern of light and dark green patches on leaf surfaces", - "Leaf distortion, curling, puckering, or unusual narrowing of leaf blades", - "Yellowing along leaf veins (vein clearing) or intervenal chlorosis", - "Reduced plant vigor, stunted growth, and poor fruit or flower set", - "Discoloration, streaking, ringspots, or deformation on fruit and flowers", - ], - causes: [ - "Virus particles transmitted by insect vectors including aphids, thrips, and whiteflies", - "Mechanical transmission through contaminated hands, pruning tools, or clothing", - "Propagation from infected parent material (cuttings, tubers, bulbs, seeds)", - "Virus overwintering in perennial weed hosts or wild reservoir plants near fields", - ], - treatment: [ - "No cure available — remove and destroy infected plants as soon as detected", - "Decontaminate tools and work surfaces with 10% bleach or trisodium phosphate", - "Wash hands thoroughly with soap and water after handling infected plants", - "Control insect vectors using reflective mulches, row covers, and registered insecticides", - "Remove weeds and alternate host plants that may harbor the virus", - ], - prevention: [ - "Purchase certified virus-free seed and transplants", - "Use insect-proof floating row covers during early growth stages", - "Isolate new plants for 2-3 weeks before introducing into the garden", - "Remove and destroy infected plants promptly at first symptom appearance", - "Rotate susceptible crops for 2-3 growing seasons", - ], - }, - environmental: { - severity: "low", - symptoms: [ - "Physiological symptoms resembling pathogen-caused disease without signs of infection", - "Symptoms often appear uniformly across planting or follow a distinct pattern", - "Tissue discoloration, necrosis, leaf margin scorch, or fruit deformation", - "Symptoms correlate with recent weather events, irrigation changes, or chemical use", - "No visible signs of fungal spores, bacterial ooze, or insect activity", - ], - causes: [ - "Environmental stress including drought, flooding, temperature extremes, or sunscald", - "Nutrient deficiencies or toxicities in soil (calcium, boron, potassium, etc.)", - "Poor soil conditions: compaction, pH imbalance, poor drainage, or salt buildup", - "Chemical injury from pesticides, herbicides, fertilizers, or air pollutants", - ], - treatment: [ - "Identify and correct the underlying environmental or nutritional issue", - "Test soil pH and nutrient levels; amend based on laboratory recommendations", - "Establish and maintain a consistent watering schedule appropriate for the crop", - "Provide shade, wind protection, or frost protection as needed for local conditions", - "Adjust fertilizer program to address specific identified nutrient deficiencies", - ], - prevention: [ - "Test soil before planting and amend to recommended pH and nutrient levels", - "Choose plant varieties well-suited to local climate and soil conditions", - "Maintain consistent irrigation, especially during fruit development and hot weather", - "Apply balanced fertilizer according to soil test recommendations", - "Improve soil drainage with raised beds or incorporation of organic matter", - ], - }, -}; - -function makeDesc(name: string, sci: string, plant: string, type: string): string { - return `${name} is a ${type} disease affecting ${plant}. Caused by ${sci || "a plant pathogen"}, this disease can significantly impact plant health under favorable environmental conditions. Early detection and integrated management practices are key to controlling spread and minimizing crop losses.`; -} - -// ─── Source definitions ────────────────────────────────────────────────────── - -interface Src { - slug: string; - name: string; - sci: string; - fam: string; - cat: string; - page: string; - care: string; - img: string; -} - -const SOURCES: Src[] = [ - { - slug: "tomato", - name: "Tomato", - sci: "Solanum lycopersicum", - fam: "Solanaceae", - cat: "vegetable", - page: "List_of_tomato_diseases", - care: "Full sun (6-8h), consistent watering, well-drained soil pH 6.0-6.8.", - img: "", - }, - { - slug: "potato", - name: "Potato", - sci: "Solanum tuberosum", - fam: "Solanaceae", - cat: "vegetable", - page: "List_of_potato_diseases", - care: "Full sun (6-8h), consistent watering, cool temps, loose soil pH 5.0-6.5.", - img: "", - }, - { - slug: "apple", - name: "Apple", - sci: "Malus domestica", - fam: "Rosaceae", - cat: "tree", - page: "List_of_apple_diseases", - care: "Full sun (8h+), deep watering weekly, well-drained soil pH 6.0-7.0.", - img: "", - }, - { - slug: "apricot", - name: "Apricot", - sci: "Prunus armeniaca", - fam: "Rosaceae", - cat: "tree", - page: "List_of_apricot_diseases", - care: "Full sun (8h+), moderate watering, well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "avocado", - name: "Avocado", - sci: "Persea americana", - fam: "Lauraceae", - cat: "tree", - page: "List_of_avocado_diseases", - care: "Full sun (6-8h), moderate watering, well-drained soil pH 5.5-7.0.", - img: "", - }, - { - slug: "banana", - name: "Banana", - sci: "Musa acuminata", - fam: "Musaceae", - cat: "fruit", - page: "List_of_banana_diseases", - care: "Full sun (8h+), consistent watering, warm temps 75-90°F.", - img: "", - }, - { - slug: "barley", - name: "Barley", - sci: "Hordeum vulgare", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_barley_diseases", - care: "Full sun (8h+), moderate watering, cool temps 55-75°F.", - img: "", - }, - { - slug: "bean", - name: "Green Bean", - sci: "Phaseolus vulgaris", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun (6-8h), moderate watering, warm temps 65-80°F.", - img: "", - }, - { - slug: "blueberry", - name: "Blueberry", - sci: "Vaccinium corymbosum", - fam: "Ericaceae", - cat: "fruit", - page: "List_of_blueberry_diseases", - care: "Full sun, consistent moisture, acidic soil pH 4.5-5.5.", - img: "", - }, - { - slug: "cabbage", - name: "Cabbage", - sci: "Brassica oleracea var. capitata", - fam: "Brassicaceae", - cat: "vegetable", - page: "List_of_brassica_diseases", - care: "Full sun, consistent watering, cool temps 50-85°F.", - img: "", - }, - { - slug: "carrot", - name: "Carrot", - sci: "Daucus carota subsp. sativus", - fam: "Apiaceae", - cat: "vegetable", - page: "List_of_carrot_diseases", - care: "Full sun, consistent moisture, cool temps, loose sandy soil.", - img: "", - }, - { - slug: "cherry", - name: "Cherry", - sci: "Prunus avium", - fam: "Rosaceae", - cat: "tree", - page: "List_of_cherry_diseases", - care: "Full sun, moderate watering, well-drained loam pH 6.0-7.0.", - img: "", - }, - { - slug: "citrus", - name: "Citrus (Orange)", - sci: "Citrus × sinensis", - fam: "Rutaceae", - cat: "tree", - page: "List_of_citrus_diseases", - care: "Full sun, consistent watering, acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "cocoa", - name: "Cocoa (Cacao)", - sci: "Theobroma cacao", - fam: "Malvaceae", - cat: "tree", - page: "List_of_cocoa_diseases", - care: "Partial shade, consistent rainfall, warm tropics 65-90°F.", - img: "", - }, - { - slug: "coconut", - name: "Coconut", - sci: "Cocos nucifera", - fam: "Arecaceae", - cat: "tree", - page: "List_of_coconut_palm_diseases", - care: "Full sun, moderate watering, warm temps 70-95°F.", - img: "", - }, - { - slug: "coffee", - name: "Coffee", - sci: "Coffea arabica", - fam: "Rubiaceae", - cat: "tree", - page: "List_of_coffee_diseases", - care: "Partial shade, consistent rainfall, moderate temps 60-70°F.", - img: "", - }, - { - slug: "corn", - name: "Corn (Maize)", - sci: "Zea mays", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_maize_diseases", - care: "Full sun, consistent watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "cranberry", - name: "Cranberry", - sci: "Vaccinium macrocarpon", - fam: "Ericaceae", - cat: "fruit", - page: "List_of_cranberry_diseases", - care: "Full sun, constant moisture, acidic soil pH 4.5-5.5.", - img: "", - }, - { - slug: "cucumber", - name: "Cucumber", - sci: "Cucumis sativus", - fam: "Cucurbitaceae", - cat: "vegetable", - page: "List_of_cucurbit_diseases", - care: "Full sun, consistent watering, warm temps 70-95°F.", - img: "", - }, - { - slug: "grape", - name: "Grape", - sci: "Vitis vinifera", - fam: "Vitaceae", - cat: "fruit", - page: "List_of_grape_diseases", - care: "Full sun, moderate watering, well-drained soil pH 5.5-7.0.", - img: "", - }, - { - slug: "hops", - name: "Hops", - sci: "Humulus lupulus", - fam: "Cannabaceae", - cat: "herb", - page: "List_of_hops_diseases", - care: "Full sun, consistent watering, well-drained soil pH 6.0-7.0.", - img: "", - }, - { - slug: "lettuce", - name: "Lettuce", - sci: "Lactuca sativa", - fam: "Asteraceae", - cat: "vegetable", - page: "List_of_lettuce_diseases", - care: "Partial shade to full sun, consistent moisture, cool temps 55-75°F.", - img: "", - }, - { - slug: "mango", - name: "Mango", - sci: "Mangifera indica", - fam: "Anacardiaceae", - cat: "tree", - page: "List_of_mango_diseases", - care: "Full sun, moderate watering, warm temps 70-100°F.", - img: "", - }, - { - slug: "oats", - name: "Oats", - sci: "Avena sativa", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_oats_diseases", - care: "Full sun, moderate watering, cool temps 50-70°F.", - img: "", - }, - { - slug: "onion", - name: "Onion", - sci: "Allium cepa", - fam: "Amaryllidaceae", - cat: "vegetable", - page: "List_of_onion_diseases", - care: "Full sun, consistent watering, cool to warm temps 55-75°F.", - img: "", - }, - { - slug: "papaya", - name: "Papaya", - sci: "Carica papaya", - fam: "Caricaceae", - cat: "fruit", - page: "List_of_papaya_diseases", - care: "Full sun, consistent watering, warm temps 70-90°F.", - img: "", - }, - { - slug: "peach", - name: "Peach", - sci: "Prunus persica", - fam: "Rosaceae", - cat: "tree", - page: "List_of_peach_diseases", - care: "Full sun, consistent watering, well-drained sandy loam pH 6.0-7.0.", - img: "", - }, - { - slug: "peanut", - name: "Peanut (Groundnut)", - sci: "Arachis hypogaea", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_peanut_diseases", - care: "Full sun, moderate watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "pear", - name: "Pear", - sci: "Pyrus communis", - fam: "Rosaceae", - cat: "tree", - page: "List_of_pear_diseases", - care: "Full sun, consistent watering, well-drained loam pH 6.0-7.0.", - img: "", - }, - { - slug: "pepper", - name: "Bell Pepper", - sci: "Capsicum annuum", - fam: "Solanaceae", - cat: "vegetable", - page: "List_of_tomato_diseases", - care: "Full sun, consistent watering, warm soil 70-80°F.", - img: "", - }, - { - slug: "pineapple", - name: "Pineapple", - sci: "Ananas comosus", - fam: "Bromeliaceae", - cat: "fruit", - page: "List_of_pineapple_diseases", - care: "Full sun, moderate watering, warm temps 65-95°F.", - img: "", - }, - { - slug: "raspberry", - name: "Raspberry", - sci: "Rubus idaeus", - fam: "Rosaceae", - cat: "fruit", - page: "List_of_raspberry_diseases", - care: "Full sun, consistent watering, slightly acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "rice", - name: "Rice", - sci: "Oryza sativa", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_rice_diseases", - care: "Full sun, flooded field conditions, warm temps 70-95°F.", - img: "", - }, - { - slug: "rose", - name: "Rose", - sci: "Rosa spp.", - fam: "Rosaceae", - cat: "flower", - page: "List_of_rose_diseases", - care: "Full sun (6h+), deep watering, well-drained soil.", - img: "", - }, - { - slug: "sorghum", - name: "Sorghum", - sci: "Sorghum bicolor", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_sorghum_diseases", - care: "Full sun, moderate watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "soybean", - name: "Soybean", - sci: "Glycine max", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_soybean_diseases", - care: "Full sun, moderate watering, warm temps 60-85°F.", - img: "", - }, - { - slug: "spinach", - name: "Spinach", - sci: "Spinacia oleracea", - fam: "Amaranthaceae", - cat: "vegetable", - page: "List_of_spinach_diseases", - care: "Partial shade to full sun, consistent moisture, cool temps 50-70°F.", - img: "", - }, - { - slug: "strawberry", - name: "Strawberry", - sci: "Fragaria × ananassa", - fam: "Rosaceae", - cat: "fruit", - page: "List_of_strawberry_diseases", - care: "Full sun, consistent watering, acidic soil pH 5.5-6.5.", - img: "", - }, - { - slug: "sugarcane", - name: "Sugarcane", - sci: "Saccharum officinarum", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_sugarcane_diseases", - care: "Full sun, heavy watering, warm temps 75-95°F.", - img: "", - }, - { - slug: "sunflower", - name: "Sunflower", - sci: "Helianthus annuus", - fam: "Asteraceae", - cat: "flower", - page: "List_of_sunflower_diseases", - care: "Full sun (6-8h+), moderate watering, warm temps 70-78°F.", - img: "", - }, - { - slug: "sweet-potato", - name: "Sweet Potato", - sci: "Ipomoea batatas", - fam: "Convolvulaceae", - cat: "vegetable", - page: "List_of_sweet_potato_diseases", - care: "Full sun, moderate watering, warm temps 65-95°F.", - img: "", - }, - { - slug: "tobacco", - name: "Tobacco", - sci: "Nicotiana tabacum", - fam: "Solanaceae", - cat: "vegetable", - page: "List_of_tobacco_diseases", - care: "Full sun, moderate watering, warm temps 65-85°F.", - img: "", - }, - { - slug: "watermelon", - name: "Watermelon", - sci: "Citrullus lanatus", - fam: "Cucurbitaceae", - cat: "vegetable", - page: "List_of_cucurbit_diseases", - care: "Full sun, consistent watering, warm temps 75-85°F.", - img: "", - }, - { - slug: "wheat", - name: "Wheat", - sci: "Triticum aestivum", - fam: "Poaceae", - cat: "vegetable", - page: "List_of_wheat_diseases", - care: "Full sun, moderate watering, cool to warm temps 55-75°F.", - img: "", - }, - { - slug: "alfalfa", - name: "Alfalfa", - sci: "Medicago sativa", - fam: "Fabaceae", - cat: "herb", - page: "List_of_alfalfa_diseases", - care: "Full sun, drought tolerant, deep well-drained soil pH 6.5-7.5.", - img: "", - }, - { - slug: "asparagus", - name: "Asparagus", - sci: "Asparagus officinalis", - fam: "Asparagaceae", - cat: "vegetable", - page: "List_of_asparagus_diseases", - care: "Full sun, consistent watering, well-drained sandy soil pH 6.5-7.5.", - img: "", - }, - { - slug: "celery", - name: "Celery", - sci: "Apium graveolens", - fam: "Apiaceae", - cat: "vegetable", - page: "List_of_celery_diseases", - care: "Full sun, consistent moisture, cool temps 55-70°F.", - img: "", - }, - { - slug: "chickpea", - name: "Chickpea", - sci: "Cicer arietinum", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun, drought tolerant, warm temps 65-85°F.", - img: "", - }, - { - slug: "clover", - name: "Clover", - sci: "Trifolium repens", - fam: "Fabaceae", - cat: "herb", - page: "List_of_clover_diseases", - care: "Full sun to partial shade, moderate watering, cool temps.", - img: "", - }, - { - slug: "cowpea", - name: "Cowpea", - sci: "Vigna unguiculata", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun, drought tolerant, warm temps 65-95°F.", - img: "", - }, - { - slug: "faba-bean", - name: "Faba Bean", - sci: "Vicia faba", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun, consistent watering, cool temps 55-70°F.", - img: "", - }, - { - slug: "lentil", - name: "Lentil", - sci: "Lens culinaris", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun, drought tolerant, cool temps 50-80°F.", - img: "", - }, - { - slug: "pigeon-pea", - name: "Pigeon Pea", - sci: "Cajanus cajan", - fam: "Fabaceae", - cat: "vegetable", - page: "List_of_legume_diseases", - care: "Full sun, drought tolerant, warm tropical temps.", - img: "", - }, - { - slug: "tea", - name: "Tea (Camellia sinensis)", - sci: "Camellia sinensis", - fam: "Theaceae", - cat: "tree", - page: "List_of_tea_diseases", - care: "Partial shade, consistent moisture, acidic soil pH 4.5-6.0.", - img: "", - }, - { - slug: "turfgrass", - name: "Turfgrass (Lawn)", - sci: "Multiple Poaceae spp.", - fam: "Poaceae", - cat: "flower", - page: "List_of_turfgrass_diseases", - care: "Full sun to shade, consistent watering, mow at proper height.", - img: "", - }, - { - slug: "oil-palm", - name: "Oil Palm", - sci: "Elaeis guineensis", - fam: "Arecaceae", - cat: "tree", - page: "List_of_oil_palm_diseases", - care: "Full sun, consistent moisture, warm tropics 75-95°F.", - img: "", - }, -]; - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main() { - console.log("🌿 Wikipedia Plant Disease Scraper\n"); - - const db = getDb(); - const totalDiseases = 0; - let totalPlants = 0; - const pageCache = new Map(); // page → tables - - // Collect unique pages with their sources - const pageToSources = new Map(); - for (const src of SOURCES) { - const list = pageToSources.get(src.page) || []; - list.push(src); - pageToSources.set(src.page, list); - } - - console.log(`🌱 ${SOURCES.length} plant entries, ${pageToSources.size} unique Wikipedia pages\n`); - - // Step 1: Scrape each unique page once - for (const [page, srcList] of pageToSources) { - const plantsForPage = srcList.map((s) => s.name).join(", "); - console.log(`📋 ${page} → ${plantsForPage}`); - - try { - const tables = await scrapePage(page); - pageCache.set(page, tables); - const totalRows = tables.reduce((s, t) => s + t.rows.length, 0); - console.log(` → ${tables.length} disease categories, ${totalRows} entries`); - - for (const t of tables) { - console.log(` ${t.type}: ${t.rows.length} diseases`); - } - } catch (err) { - console.error(` ❌ ${err instanceof Error ? err.message : err}`); - } - } - - // Step 2: Build all disease entries per plant - interface DiseaseEntry { - id: string; - plantId: string; - name: string; - scientificName: string; - causalAgentType: CausalAgentType; - description: string; - symptoms: string[]; - causes: string[]; - treatment: string[]; - prevention: string[]; - lookalikeIds: string[]; - severity: Severity; - sourceUrl: string; - } - - const allDiseases: DiseaseEntry[] = []; - const insertedPlants = new Set(); - - for (const src of SOURCES) { - // Insert plant if not already - if (!insertedPlants.has(src.slug)) { - insertedPlants.add(src.slug); - totalPlants++; - await db - .insert(plants) - .values({ - id: src.slug, - commonName: src.name, - scientificName: src.sci, - family: src.fam, - category: src.cat, - careSummary: src.care, - imageUrl: src.img, - }) - .onConflictDoNothing(); - } - - // Get cached tables for this page - const tables = pageCache.get(src.page); - if (!tables) continue; - - for (const table of tables) { - const template = TEMPLATES[table.type]; - for (const row of table.rows) { - const diseaseId = `${src.slug}-${slugify(row.name)}`; - - allDiseases.push({ - id: diseaseId, - plantId: src.slug, - name: row.name, - scientificName: row.sci, - causalAgentType: table.type, - description: makeDesc(row.name, row.sci, src.name, table.type), - symptoms: template.symptoms, - causes: template.causes, - treatment: template.treatment, - prevention: template.prevention, - lookalikeIds: [], - severity: template.severity, - sourceUrl: `https://en.wikipedia.org/wiki/${src.page}`, - }); - } - } - } - - // Step 3: Link lookalikes (same plant, same type) - const byPlant = new Map(); - for (const d of allDiseases) { - const list = byPlant.get(d.plantId) || []; - list.push(d); - byPlant.set(d.plantId, list); - } - for (const [, di] of byPlant) { - for (const d of di) { - if (d.severity === "low") continue; - const sameType = di.filter((o) => o.causalAgentType === d.causalAgentType && o.id !== d.id); - d.lookalikeIds = sameType.slice(0, 3).map((o) => o.id); - } - } - - console.log( - `\n📊 Total: ${totalDiseases + allDiseases.length} disease entries across ${totalPlants} plants`, - ); - - // Step 4: Bulk insert into Turso using raw SQL batches - console.log("\n💾 Inserting into Turso via batch..."); - const BATCH_SIZE = 100; - let inserted = 0; - - // Use the raw libsql client for batch operations - const { createClient } = await import("@libsql/client"); - const rawClient = createClient({ - url: process.env.DATABASE_URL!, - authToken: process.env.DATABASE_TOKEN!, - }); - - for (let i = 0; i < allDiseases.length; i += BATCH_SIZE) { - const chunk = allDiseases.slice(i, i + BATCH_SIZE); - const stmts = chunk.map((d) => ({ - sql: `INSERT OR IGNORE INTO diseases (id, plant_id, name, scientific_name, causal_agent_type, description, symptoms, causes, treatment, prevention, lookalike_ids, severity, source_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - args: [ - d.id, - d.plantId, - d.name, - d.scientificName, - d.causalAgentType, - d.description, - JSON.stringify(d.symptoms), - JSON.stringify(d.causes), - JSON.stringify(d.treatment), - JSON.stringify(d.prevention), - JSON.stringify(d.lookalikeIds), - d.severity, - d.sourceUrl, - ], - })); - - await rawClient.batch(stmts, "write"); - inserted += chunk.length; - process.stdout.write(` ${Math.min(inserted, allDiseases.length)}/${allDiseases.length}\n`); - } - - rawClient.close(); - - // Log scrape - await db - .insert(scrapeSources) - .values({ - id: "wikipedia-scrape", - sourceType: "wikipedia", - sourceUrl: "https://en.wikipedia.org/wiki/Category:Plant_pathogens_and_diseases", - entriesCount: allDiseases.length, - status: "success", - lastScrapedAt: new Date().toISOString(), - }) - .onConflictDoUpdate({ - target: scrapeSources.id, - set: { - entriesCount: allDiseases.length, - status: "success" as const, - lastScrapedAt: new Date().toISOString(), - }, - }); - - // Stats - const [pc] = await db.select({ c: sql`COUNT(*)` }).from(plants); - const [dc] = await db.select({ c: sql`COUNT(*)` }).from(diseases); - console.log(`\n✅ Done! Database: ${pc.c} plants, ${dc.c} diseases`); - closeDb(); -} - -main().catch((err) => { - console.error("❌", err); - process.exit(1); -}); diff --git a/scripts/seed-existing.ts b/scripts/seed-existing.ts deleted file mode 100644 index 3c8a9b4..0000000 --- a/scripts/seed-existing.ts +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env node -/** - * Seed Existing JSON Data into Turso - * - * Reads the existing plants.json and diseases.json files and inserts them - * into the Turso database via Drizzle ORM. - * - * Usage: - * cd apps/web && npx tsx scripts/seed-existing.ts - * - * Environment: DATABASE_URL and DATABASE_TOKEN from .env.development - */ - -import "dotenv/config"; -import { readFileSync } from "fs"; -import { resolve } from "path"; -import { sql } from "drizzle-orm"; -import { getDb, closeDb } from "../src/lib/db/index"; -import { plants, diseases } from "../src/lib/db/schema"; -import type { Plant, Disease } from "../src/lib/types"; - -// ─── Load JSON data ────────────────────────────────────────────────────────── - -const __dirname = resolve(new URL(".", import.meta.url).pathname); - -const plantsPath = resolve(__dirname, "../src/data/plants.json"); -const diseasesPath = resolve(__dirname, "../src/data/diseases.json"); - -const rawPlants = JSON.parse(readFileSync(plantsPath, "utf-8")) as Plant[]; -const rawDiseases = JSON.parse(readFileSync(diseasesPath, "utf-8")) as Disease[]; - -// ─── Seed ──────────────────────────────────────────────────────────────────── - -async function main() { - const db = getDb(); - - console.log(`Seeding ${rawPlants.length} plants...`); - for (const p of rawPlants) { - await db - .insert(plants) - .values({ - id: p.id, - commonName: p.commonName, - scientificName: p.scientificName, - family: p.family, - category: p.category, - careSummary: p.careSummary, - imageUrl: p.imageUrl, - }) - .onConflictDoNothing(); - } - console.log(`✅ ${rawPlants.length} plants inserted`); - - console.log(`Seeding ${rawDiseases.length} diseases...`); - for (const d of rawDiseases) { - await db - .insert(diseases) - .values({ - id: d.id, - plantId: d.plantId, - name: d.name, - scientificName: d.scientificName, - causalAgentType: d.causalAgentType, - description: d.description, - symptoms: d.symptoms, - causes: d.causes, - treatment: d.treatment, - prevention: d.prevention, - lookalikeIds: d.lookalikeDiseaseIds, - severity: d.severity, - prevalence: d.prevalence ?? "uncommon", - sourceUrl: "", - }) - .onConflictDoNothing(); - } - console.log(`✅ ${rawDiseases.length} diseases inserted`); - - // Verify - const [plantCount] = await db.select({ count: sql`COUNT(*)` }).from(plants); - const [diseaseCount] = await db.select({ count: sql`COUNT(*)` }).from(diseases); - console.log(`\n📊 Database now has:`); - console.log(` ${plantCount.count} plants`); - console.log(` ${diseaseCount.count} diseases`); - - closeDb(); -} - -main().catch((err) => { - console.error("❌ Seed failed:", err); - process.exit(1); -}); diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs deleted file mode 100644 index 2cc38f6..0000000 --- a/scripts/smoke-test.mjs +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env node -/** - * Smoke test script for the Plant Disease Knowledge Base API. - * Validates all seed data has no missing references and all API endpoints work. - * - * Usage: - * # With dev server running: - * node scripts/smoke-test.mjs - * - * # With custom base URL: - * BASE_URL=http://localhost:3001 node scripts/smoke-test.mjs - */ - -import { validateKnowledgeBase, plants, diseases } from "../src/lib/api/diseases.ts"; - -const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; -const results = { passed: 0, failed: 0, errors: [] }; - -function pass(test) { - results.passed++; - console.log(` ✅ ${test}`); -} - -function fail(test, message) { - results.failed++; - results.errors.push({ test, message }); - console.log(` ❌ ${test}: ${message}`); -} - -async function fetchJSON(path) { - const res = await fetch(`${BASE_URL}${path}`); - const data = await res.json(); - return { status: res.status, data, headers: Object.fromEntries(res.headers) }; -} - -console.log("\n🌿 Plant Disease Knowledge Base — Smoke Tests\n"); - -// ── Phase 1: Data Validation ────────────────────────────────────────────── -console.log("Phase 1: Seed Data Validation"); - -const validationErrors = validateKnowledgeBase(); -if (validationErrors.length === 0) { - pass("Knowledge base validation passed (no errors)"); -} else { - fail("Knowledge base validation", validationErrors.join("; ")); -} - -if (plants.length >= 20) { - pass(`Plant count: ${plants.length} (≥20)`); -} else { - fail("Plant count", `Only ${plants.length} plants (need ≥20)`); -} - -if (diseases.length >= 80) { - pass(`Disease count: ${diseases.length} (≥80)`); -} else { - fail("Disease count", `Only ${diseases.length} diseases (need ≥80)`); -} - -const uniquePlantIds = new Set(diseases.map((d) => d.plantId)); -if (uniquePlantIds.size >= 20) { - pass(`Diseases span ${uniquePlantIds.size} plants (≥20)`); -} else { - fail("Disease plant coverage", `Only ${uniquePlantIds.size} plants have diseases`); -} - -const causalTypes = new Set(diseases.map((d) => d.causalAgentType)); -if (causalTypes.size === 4) { - pass(`All 4 causal agent types present: ${[...causalTypes].join(", ")}`); -} else { - fail("Causal agent types", `Only ${causalTypes.size}/4 types present`); -} - -// ── Phase 2: API Endpoint Tests ─────────────────────────────────────────── -console.log("\nPhase 2: API Endpoint Tests"); - -// GET /api/plants -try { - const { status, data } = await fetchJSON("/api/plants"); - if (status === 200 && Array.isArray(data.plants) && data.plants.length >= 20) { - pass(`GET /api/plants returns 200 with ${data.plants.length} plants`); - } else { - fail("GET /api/plants", `Status ${status}, plants: ${data.plants?.length ?? "N/A"}`); - } -} catch (e) { - fail("GET /api/plants", e.message); -} - -// GET /api/plants?search=tomato -try { - const { status, data } = await fetchJSON("/api/plants?search=tomato"); - if (status === 200 && data.plants.length > 0) { - pass(`GET /api/plants?search=tomato returns ${data.plants.length} results`); - } else { - fail("GET /api/plants?search=tomato", `Status ${status}`); - } -} catch (e) { - fail("GET /api/plants?search=tomato", e.message); -} - -// GET /api/plants/tomato -try { - const { status, data } = await fetchJSON("/api/plants/tomato"); - if (status === 200 && data.plant?.id === "tomato" && data.diseases?.length >= 3) { - pass(`GET /api/plants/tomato returns 200 with ${data.diseases.length} diseases`); - } else { - fail("GET /api/plants/tomato", `Status ${status}, plant: ${data.plant?.id ?? "N/A"}`); - } -} catch (e) { - fail("GET /api/plants/tomato", e.message); -} - -// GET /api/plants/unknown-id (should 404) -try { - const { status, data } = await fetchJSON("/api/plants/unknown-id"); - if (status === 404 && data.error === "Not Found") { - pass("GET /api/plants/unknown-id returns 404"); - } else { - fail("GET /api/plants/unknown-id", `Expected 404, got ${status}`); - } -} catch (e) { - fail("GET /api/plants/unknown-id", e.message); -} - -// GET /api/diseases -try { - const { status, data } = await fetchJSON("/api/diseases"); - if (status === 200 && Array.isArray(data.diseases) && data.diseases.length >= 80) { - pass(`GET /api/diseases returns 200 with ${data.diseases.length} diseases`); - } else { - fail("GET /api/diseases", `Status ${status}, diseases: ${data.diseases?.length ?? "N/A"}`); - } -} catch (e) { - fail("GET /api/diseases", e.message); -} - -// GET /api/diseases?plantId=tomato -try { - const { status, data } = await fetchJSON("/api/diseases?plantId=tomato"); - if (status === 200 && data.diseases.length >= 3 && data.diseases.every((d) => d.plantId === "tomato")) { - pass(`GET /api/diseases?plantId=tomato returns ${data.diseases.length} tomato diseases`); - } else { - fail("GET /api/diseases?plantId=tomato", `Status ${status}, count: ${data.diseases?.length ?? "N/A"}`); - } -} catch (e) { - fail("GET /api/diseases?plantId=tomato", e.message); -} - -// GET /api/diseases?search=blight -try { - const { status, data } = await fetchJSON("/api/diseases?search=blight"); - if (status === 200 && data.diseases.length >= 2) { - pass(`GET /api/diseases?search=blight returns ${data.diseases.length} results (≥2)`); - } else { - fail("GET /api/diseases?search=blight", `Status ${status}, count: ${data.diseases?.length ?? "N/A"}`); - } -} catch (e) { - fail("GET /api/diseases?search=blight", e.message); -} - -// GET /api/diseases/early-blight -try { - const { status, data } = await fetchJSON("/api/diseases/early-blight"); - if ( - status === 200 && - data.disease?.id === "early-blight" && - data.plant?.id === "tomato" && - Array.isArray(data.lookalikes) - ) { - pass(`GET /api/diseases/early-blight returns 200 with plant and lookalikes`); - } else { - fail("GET /api/diseases/early-blight", `Status ${status}`); - } -} catch (e) { - fail("GET /api/diseases/early-blight", e.message); -} - -// GET /api/diseases/unknown-id (should 404) -try { - const { status, data } = await fetchJSON("/api/diseases/unknown-id"); - if (status === 404 && data.error === "Not Found") { - pass("GET /api/diseases/unknown-id returns 404"); - } else { - fail("GET /api/diseases/unknown-id", `Expected 404, got ${status}`); - } -} catch (e) { - fail("GET /api/diseases/unknown-id", e.message); -} - -// ── Phase 3: Response Headers ───────────────────────────────────────────── -console.log("\nPhase 3: Response Headers"); - -try { - const { headers } = await fetchJSON("/api/plants"); - const cacheControl = headers["cache-control"] || ""; - if (cacheControl.includes("max-age=3600")) { - pass(`Cache-Control header present: ${cacheControl}`); - } else { - fail("Cache-Control header", `Expected max-age=3600, got: ${cacheControl}`); - } -} catch (e) { - fail("Cache-Control header", e.message); -} - -// ── Summary ─────────────────────────────────────────────────────────────── -console.log("\n" + "─".repeat(50)); -console.log(`Results: ${results.passed} passed, ${results.failed} failed`); - -if (results.failed > 0) { - console.log("\nFailed tests:"); - for (const { test, message } of results.errors) { - console.log(` • ${test}: ${message}`); - } - process.exit(1); -} else { - console.log("\n🎉 All smoke tests passed!\n"); - process.exit(0); -} diff --git a/scripts/test-wiki-images.ts b/scripts/test-wiki-images.ts deleted file mode 100644 index 017c1a7..0000000 --- a/scripts/test-wiki-images.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Quick test of Wikipedia image API for disease search terms. - * Run: cd apps/web && npx tsx scripts/test-wiki-images.ts - */ -const API = "https://en.wikipedia.org/w/api.php"; - -async function search(term: string) { - const url = `${API}?action=query&list=search&srsearch=${encodeURIComponent(term)}&format=json&srlimit=1&origin=*`; - const res = await fetch(url, { headers: { "User-Agent": "PlantHealthKB/1.0" } }); - return (await res.json()) as { query?: { search?: Array<{ title: string; pageid: number }> } }; -} - -async function getImg(title: string) { - const url = `${API}?action=query&titles=${encodeURIComponent(title)}&prop=pageimages&format=json&pithumbsize=400&origin=*`; - const res = await fetch(url, { headers: { "User-Agent": "PlantHealthKB/1.0" } }); - return (await res.json()) as { - query?: { pages?: Record }; - }; -} - -async function testOne(term: string) { - const s = await search(term); - const page = s?.query?.search?.[0]; - if (page) { - const img = await getImg(page.title); - const pages = img?.query?.pages; - if (!pages) { - console.log(term, "→ NO PAGES"); - return; - } - const first = Object.values(pages)[0] as { thumbnail?: { source: string } }; - const thumb = first?.thumbnail?.source; - console.log(`${term.padEnd(40)} → ${page.title.padEnd(50)} → ${thumb ?? "NO IMG"}`); - } else { - console.log(`${term.padEnd(40)} → NO PAGE`); - } - await new Promise((r) => setTimeout(r, 400)); -} - -async function main() { - const tests = [ - "Phytophthora infestans Late Blight", - "Early Blight", - "Septoria Leaf Spot", - "Powdery Mildew", - "Fusarium oxysporum", - "Citrus Canker", - "Root Rot Pythium", - "Downy Mildew Peronospora", - "Bacterial Leaf Spot Xanthomonas", - "Apple Scab Venturia inaequalis", - "Fire Blight Erwinia amylovora", - "Blossom End Rot", - "Tomato Mosaic Virus", - "Rust Puccinia", - "Black Spot Diplocarpon rosae", - "Sooty Mold Capnodium", - "Clubroot Plasmodiophora brassicae", - "Anthracnose Colletotrichum", - ]; - console.log("Searching Wikipedia for disease images...\n"); - for (const t of tests) { - await testOne(t); - } -} - -main().catch(console.error);