diff --git a/backend/main.py b/backend/main.py index 000c432..97eb0b2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -118,13 +118,16 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) resnet_model.fc = nn.Linear(resnet_model.fc.in_features, NUM_CLASSES) MODEL_PATH = os.path.join(BASE_DIR, "model", "plant_disease_resnet18.pth") -if os.path.exists(MODEL_PATH): - resnet_model.load_state_dict(torch.load(MODEL_PATH, map_location=device)) - resnet_model = resnet_model.to(device) - resnet_model.eval() - print(f"[OK] ResNet18 loaded - {NUM_CLASSES} classes on {device}") +if os.path.exists(MODEL_PATH) and os.path.getsize(MODEL_PATH) > 0: + try: + resnet_model.load_state_dict(torch.load(MODEL_PATH, map_location=device)) + resnet_model = resnet_model.to(device) + resnet_model.eval() + print(f"[OK] ResNet18 loaded -- {NUM_CLASSES} classes on {device}") + except Exception as e: + print(f"[WARN] Failed to load model weights: {e}. Inference will fail.") else: - print(f"[WARN] Model weights not found at {MODEL_PATH}. Inference will fail.") + print(f"[WARN] Model weights not found or empty at {MODEL_PATH}. Inference will fail.") # CLIP model (OOD guard) — lazy load to keep startup fast clip_model = None @@ -220,12 +223,23 @@ class YieldCreate(BaseModel): quality_notes: Optional[str] = None class FieldCreate(BaseModel): + name: str + area_acres: float + polygon: List[Dict[str, float]] + center_latitude: Optional[float] = None + center_longitude: Optional[float] = None + + +class FieldResponse(BaseModel): + id: str + farm_id: str name: str area_acres: float area_hectares: Optional[float] = None polygon: List[Dict[str, float]] center_latitude: Optional[float] = None center_longitude: Optional[float] = None + created_at: Optional[str] = None class SoilEstimationRequest(BaseModel): farm_id: str @@ -642,51 +656,87 @@ async def delete_farm(farm_id: str, user=Depends(verify_token)): # ── Farm Fields (Polygon) ───────────────────────────────────────────────────── @app.get("/api/farms/{farm_id}/fields") -async def get_fields(farm_id: str, user=Depends(verify_token)): - res = supabase.table("farms").select("location").eq("id", farm_id).eq("farmer_id", user.id).limit(1).execute() - if not res.data: +async def get_fields( + farm_id: str, + user: dict = Depends(verify_token), +) -> dict: + # Verify farm ownership before returning fields + owned = ( + supabase.table("farms") + .select("id") + .eq("id", farm_id) + .eq("farmer_id", user.id) + .limit(1) + .execute() + ) + if not owned.data: raise HTTPException(status_code=404, detail="Farm not found") - fields = (res.data.get("location") or {}).get("fields", []) - return {"success": True, "data": fields} + + res = ( + supabase.table("farm_fields") + .select("*") + .eq("farm_id", farm_id) + .order("created_at") + .execute() + ) + return {"success": True, "data": res.data or []} + @app.post("/api/farms/{farm_id}/fields", status_code=201) -async def add_field(farm_id: str, body: FieldCreate, user=Depends(verify_token)): - owned = supabase.table("farms").select("location, total_area").eq("id", farm_id).eq("farmer_id", user.id).limit(1).execute() +async def add_field( + farm_id: str, + body: FieldCreate, + user: dict = Depends(verify_token), +) -> dict: + # Verify farm ownership + owned = ( + supabase.table("farms") + .select("id") + .eq("id", farm_id) + .eq("farmer_id", user.id) + .limit(1) + .execute() + ) if not owned.data: raise HTTPException(status_code=403, detail="Access denied") - current_loc = owned.data[0][0].get("location") or {} - current_fields = current_loc.get("fields", []) - - new_field = { - "id": str(uuid.uuid4()), - **body.model_dump(), + payload: dict = { + "farm_id": farm_id, + "name": body.name, + "area_acres": body.area_acres, + "polygon": body.polygon, + "center_latitude": body.center_latitude, + "center_longitude": body.center_longitude, } - updated_fields = current_fields + [new_field] - total_area = sum(f.get("area_acres", 0) for f in updated_fields) + # Strip None values so DB defaults apply + payload = {k: v for k, v in payload.items() if v is not None} + + res = supabase.table("farm_fields").insert(payload).execute() + if not res.data: + raise HTTPException(status_code=500, detail="Failed to save field") - supabase.table("farms").update({ - "location": {**current_loc, "fields": updated_fields}, - "total_area": total_area, - }).eq("id", farm_id).execute() + return {"success": True, "data": res.data[0]} - return {"success": True, "data": new_field} @app.delete("/api/farms/{farm_id}/fields/{field_id}") -async def delete_field(farm_id: str, field_id: str, user=Depends(verify_token)): - owned = supabase.table("farms").select("location, total_area").eq("id", farm_id).eq("farmer_id", user.id).limit(1).execute() +async def delete_field( + farm_id: str, + field_id: str, + user: dict = Depends(verify_token), +) -> dict: + # Verify farm ownership + owned = ( + supabase.table("farms") + .select("id") + .eq("id", farm_id) + .eq("farmer_id", user.id) + .limit(1) + .execute() + ) if not owned.data: raise HTTPException(status_code=403, detail="Access denied") - current_loc = owned.data[0][0].get("location") or {} - current_fields = current_loc.get("fields", []) - updated_fields = [f for f in current_fields if f.get("id") != field_id] - total_area = sum(f.get("area_acres", 0) for f in updated_fields) or owned.data[0][0].get("total_area", 0) - - supabase.table("farms").update({ - "location": {**current_loc, "fields": updated_fields}, - "total_area": total_area, - }).eq("id", farm_id).execute() + supabase.table("farm_fields").delete().eq("id", field_id).eq("farm_id", farm_id).execute() return {"success": True, "message": "Field deleted"} diff --git a/backend/supabase/migrations/20260607000000_farm_fields_table.sql b/backend/supabase/migrations/20260607000000_farm_fields_table.sql new file mode 100644 index 0000000..3a450bf --- /dev/null +++ b/backend/supabase/migrations/20260607000000_farm_fields_table.sql @@ -0,0 +1,198 @@ +-- ============================================================================= +-- AgroNavis — Migration 6: Dedicated farm_fields table +-- Applied: 2026-06-07 +-- +-- Replaces the JSONB fields array embedded in farms.location with a proper +-- relational table. This enables: +-- • RLS policies scoped to individual fields +-- • Indexed spatial lookups by farm +-- • Clean foreign-key cascade deletes +-- • Accurate total_area derived via DB aggregation +-- +-- Backwards compatibility: farms.location.polygon (legacy single-polygon) +-- is left untouched so existing data continues to render in FarmMap. +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 1. Create farm_fields table +-- ----------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS public.farm_fields ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + farm_id uuid NOT NULL REFERENCES public.farms(id) ON DELETE CASCADE, + + name text NOT NULL, + area_acres numeric NOT NULL CHECK (area_acres > 0), + area_hectares numeric GENERATED ALWAYS AS (area_acres * 0.404686) STORED, + + -- Polygon stored as JSONB array of {lat, lng} points + -- e.g. [{"lat": 22.57, "lng": 88.36}, ...] + polygon jsonb NOT NULL, + + center_latitude numeric, + center_longitude numeric, + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT farm_fields_pkey PRIMARY KEY (id) +); + +-- Index: fast lookup of all fields for a given farm +CREATE INDEX IF NOT EXISTS idx_farm_fields_farm_id + ON public.farm_fields (farm_id); + +-- Index: spatial-ish lookup by centre coordinates +CREATE INDEX IF NOT EXISTS idx_farm_fields_center + ON public.farm_fields (center_latitude, center_longitude) + WHERE center_latitude IS NOT NULL AND center_longitude IS NOT NULL; + + +-- ----------------------------------------------------------------------------- +-- 2. updated_at trigger +-- ----------------------------------------------------------------------------- + +CREATE TRIGGER set_farm_fields_updated_at + BEFORE UPDATE ON public.farm_fields + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + + +-- ----------------------------------------------------------------------------- +-- 3. Row Level Security +-- Farmers can only see and modify fields that belong to their own farms. +-- ----------------------------------------------------------------------------- + +ALTER TABLE public.farm_fields ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Farmers can view fields on own farms" + ON public.farm_fields FOR SELECT + USING ( + auth.uid() IN ( + SELECT farmer_id FROM public.farms WHERE id = farm_id + ) + ); + +CREATE POLICY "Farmers can insert fields on own farms" + ON public.farm_fields FOR INSERT + WITH CHECK ( + auth.uid() IN ( + SELECT farmer_id FROM public.farms WHERE id = farm_id + ) + ); + +CREATE POLICY "Farmers can update fields on own farms" + ON public.farm_fields FOR UPDATE + USING ( + auth.uid() IN ( + SELECT farmer_id FROM public.farms WHERE id = farm_id + ) + ); + +CREATE POLICY "Farmers can delete fields on own farms" + ON public.farm_fields FOR DELETE + USING ( + auth.uid() IN ( + SELECT farmer_id FROM public.farms WHERE id = farm_id + ) + ); + + +-- ----------------------------------------------------------------------------- +-- 4. Auto-update farms.total_area when fields change +-- Keeps the farms row in sync without needing application-level bookkeeping. +-- ----------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION public.sync_farm_total_area() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_farm_id uuid; + v_total numeric; +BEGIN + -- Determine the affected farm_id from either NEW or OLD row + v_farm_id := COALESCE(NEW.farm_id, OLD.farm_id); + + SELECT COALESCE(SUM(area_acres), 0) + INTO v_total + FROM public.farm_fields + WHERE farm_id = v_farm_id; + + UPDATE public.farms + SET total_area = v_total + WHERE id = v_farm_id; + + RETURN COALESCE(NEW, OLD); +END; +$$; + +CREATE TRIGGER trg_sync_farm_total_area + AFTER INSERT OR UPDATE OR DELETE ON public.farm_fields + FOR EACH ROW + EXECUTE FUNCTION public.sync_farm_total_area(); + + +-- ----------------------------------------------------------------------------- +-- 5. Migrate existing JSONB fields into the new table +-- Reads farms.location->>'fields' and inserts each entry as a proper row. +-- Safe to run even if the JSONB array is empty or missing. +-- ----------------------------------------------------------------------------- + +DO $$ +DECLARE + r RECORD; + field_row jsonb; +BEGIN + FOR r IN + SELECT id, location + FROM public.farms + WHERE location IS NOT NULL + AND location->'fields' IS NOT NULL + AND jsonb_array_length(location->'fields') > 0 + LOOP + FOR field_row IN + SELECT jsonb_array_elements(r.location->'fields') + LOOP + INSERT INTO public.farm_fields ( + id, + farm_id, + name, + area_acres, + polygon, + center_latitude, + center_longitude + ) VALUES ( + COALESCE((field_row->>'id')::uuid, gen_random_uuid()), + r.id, + COALESCE(field_row->>'name', 'Unnamed Field'), + COALESCE((field_row->>'area_acres')::numeric, 0), + COALESCE(field_row->'polygon', '[]'::jsonb), + (field_row->>'center_latitude')::numeric, + (field_row->>'center_longitude')::numeric + ) + ON CONFLICT (id) DO NOTHING; + END LOOP; + END LOOP; +END; +$$; + + +-- ----------------------------------------------------------------------------- +-- 6. Grant service_role access (used by FastAPI backend) +-- ----------------------------------------------------------------------------- + +GRANT SELECT, INSERT, UPDATE, DELETE ON public.farm_fields TO service_role; + + +-- ============================================================================= +-- DONE +-- Table: farm_fields (id, farm_id, name, area_acres, area_hectares, +-- polygon, center_latitude, center_longitude, +-- created_at, updated_at) +-- Trigger: trg_sync_farm_total_area — keeps farms.total_area accurate +-- Trigger: set_farm_fields_updated_at — auto-updates updated_at +-- RLS: SELECT / INSERT / UPDATE / DELETE scoped to farm owner +-- Index: idx_farm_fields_farm_id, idx_farm_fields_center +-- Migration: existing JSONB fields copied into new table +-- ============================================================================= diff --git a/backend/supabase/seed.sql b/backend/supabase/seed.sql new file mode 100644 index 0000000..b6f2091 --- /dev/null +++ b/backend/supabase/seed.sql @@ -0,0 +1,76 @@ +-- AgroNavis local development seed +-- Creates the dev bypass user referenced in .env.example +-- Runs automatically on: npx supabase db reset +-- +-- Credentials: dev@agronavis.local / password123 +-- Matches NEXT_PUBLIC_DEV_EMAIL / NEXT_PUBLIC_DEV_PASSWORD in .env +-- DO NOT add real credentials or production data here. + +DO $$ +DECLARE + dev_user_id uuid := gen_random_uuid(); +BEGIN + -- Only insert if the dev user doesn't already exist + IF NOT EXISTS ( + SELECT 1 FROM auth.users WHERE email = 'dev@agronavis.local' + ) THEN + INSERT INTO auth.users ( + id, + instance_id, + aud, + role, + email, + encrypted_password, + email_confirmed_at, + raw_app_meta_data, + raw_user_meta_data, + created_at, + updated_at, + confirmation_token, + recovery_token, + email_change_token_new, + email_change + ) VALUES ( + dev_user_id, + '00000000-0000-0000-0000-000000000000', + 'authenticated', + 'authenticated', + 'dev@agronavis.local', + -- bcrypt hash of 'password123' + crypt('password123', gen_salt('bf')), + now(), + '{"provider":"email","providers":["email"]}', + '{"email_verified":true}', + now(), + now(), + '', + '', + '', + '' + ); + + INSERT INTO auth.identities ( + id, + user_id, + identity_data, + provider, + provider_id, + last_sign_in_at, + created_at, + updated_at + ) VALUES ( + gen_random_uuid(), + dev_user_id, + json_build_object('sub', dev_user_id::text, 'email', 'dev@agronavis.local'), + 'email', + dev_user_id::text, + now(), + now(), + now() + ); + + RAISE NOTICE 'Dev user created: dev@agronavis.local'; + ELSE + RAISE NOTICE 'Dev user already exists, skipping.'; + END IF; +END $$; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3458faf..71d55ed 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@turf/turf": "^7.3.4", "@types/leaflet": "^1.9.20", "@types/turf": "^3.5.32", + "Agronavis": "file:..", "axios": "^1.6.2", "dotenv": "^17.4.2", "i18next": "^25.6.0", @@ -52,6 +53,14 @@ "ts-node": "^10.9.2" } }, + "..": { + "name": "Agronavis", + "version": "2.0.0", + "devDependencies": { + "concurrently": "^8.2.2", + "supabase": "^2.105.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", @@ -6928,6 +6937,10 @@ "node": ">= 14" } }, + "node_modules/Agronavis": { + "resolved": "..", + "link": true + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index fe58917..d471401 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@turf/turf": "^7.3.4", "@types/leaflet": "^1.9.20", "@types/turf": "^3.5.32", + "Agronavis": "file:..", "axios": "^1.6.2", "dotenv": "^17.4.2", "i18next": "^25.6.0", diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index e162565..a5165b7 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -7,6 +7,7 @@ import s from '../styles/Dashboard.module.css'; import { farmApi, cropApi } from '../utils/farmApi'; import { soilService } from '../utils/soilService'; import { useReverseGeocode } from '../hooks/useReverseGeocode'; +import { FIELD_COLORS } from '../utils/mapUtils'; import type { LatLng } from '../utils/geoUtils'; import ProfileTab from './ProfileTab'; import AnalyticsDashboard from './AnalyticsDashboard'; @@ -20,6 +21,16 @@ const PolygonMapper = dynamic(() => import('./map/PolygonMapper'), { loading: () =>
Loading map...
, }); +interface Field { + id: string; + name: string; + area_acres: number; + area_hectares?: number; + polygon: Array<{ lat: number; lng: number }>; + center_latitude?: number; + center_longitude?: number; +} + interface Farm { id: string; name: string; @@ -87,13 +98,18 @@ const FertDot: React.FC<{ color: string }> = ({ color }) => ( }} /> ); +// Palette for field colour swatches — imported from shared mapUtils +// (FIELD_COLORS is used by both Dashboard and FarmMap to keep colours in sync) + const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { const router = useRouter(); const { t } = useTranslation(); const [farms, setFarms] = useState([]); const [crops, setCrops] = useState([]); const [selectedFarmId, setSelectedFarmId] = useState(''); - const [polygonData, setPolygonData] = useState<{ + const [fields, setFields] = useState([]); + const [pendingField, setPendingField] = useState<{ + fieldName: string; coordinates: LatLng[]; areaAcres: number; areaHectares: number; @@ -101,6 +117,7 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { centerLng: number; } | null>(null); const [saving, setSaving] = useState(false); + const [deletingFieldId, setDeletingFieldId] = useState(null); const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null); const [loading, setLoading] = useState(true); @@ -134,6 +151,14 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { useEffect(() => { loadData(); }, [loadData]); + // Reload fields from the dedicated table whenever the selected farm changes + useEffect(() => { + if (!selectedFarmId) return; + farmApi.getFarmFields(selectedFarmId) + .then((data: Field[]) => setFields(data || [])) + .catch(() => setFields([])); + }, [selectedFarmId]); + // Handle ?mode=draw from onboarding redirect useEffect(() => { const mode = router.query.mode as string; @@ -146,41 +171,60 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { }, [router.query, farms]); // Map handlers - const handleSavePolygon = async () => { - if (!polygonData || !selectedFarmId) return; + const handleAddField = async () => { + if (!pendingField || !selectedFarmId) return; setSaving(true); setSaveMessage(null); try { - await farmApi.updateFarm(selectedFarmId, { - total_area: polygonData.areaAcres, - latitude: polygonData.centerLat, - longitude: polygonData.centerLng, - location: { - ...selectedFarm?.location, - polygon: polygonData.coordinates.map(p => ({ lat: p.lat, lng: p.lng })), - center_latitude: polygonData.centerLat, - center_longitude: polygonData.centerLng, - area_acres: polygonData.areaAcres, - area_hectares: polygonData.areaHectares, - }, + await farmApi.addFarmField(selectedFarmId, { + name: pendingField.fieldName, + area_acres: pendingField.areaAcres, + area_hectares: pendingField.areaHectares, + polygon: pendingField.coordinates.map(p => ({ lat: p.lat, lng: p.lng })), + center_latitude: pendingField.centerLat, + center_longitude: pendingField.centerLng, }); - setSaveMessage({ type: 'success', text: `Boundary saved — ${polygonData.areaAcres.toFixed(1)} acres` }); + setSaveMessage({ type: 'success', text: `"${pendingField.fieldName}" saved — ${pendingField.areaAcres.toFixed(1)} acres` }); try { - const geo = await geocode(polygonData.centerLat, polygonData.centerLng); + const geo = await geocode(pendingField.centerLat, pendingField.centerLng); if (geo?.state && geo?.district) { await soilService.estimateSoilHealth(selectedFarmId, geo.state, geo.district); - setSaveMessage({ type: 'success', text: `Boundary saved & soil analysed for ${geo.district}` }); } - } catch { /* silent */ } + } catch { /* silent — soil estimation is best-effort */ } + // Refresh both farms (for total_area) and the fields list await loadData(); - setTimeout(() => { setPolygonData(null); setSaveMessage(null); }, 2500); - } catch (err: any) { - setSaveMessage({ type: 'error', text: err.message || 'Failed to save boundary' }); + const refreshed = await farmApi.getFarmFields(selectedFarmId); + setFields(refreshed || []); + setPendingField(null); + setTimeout(() => setSaveMessage(null), 3000); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Failed to save field'; + setSaveMessage({ type: 'error', text: msg }); } finally { setSaving(false); } }; + const handleDeleteField = async (fieldId: string, fieldName: string) => { + if (!selectedFarmId) return; + setDeletingFieldId(fieldId); + setSaveMessage(null); + try { + await farmApi.deleteFarmField(selectedFarmId, fieldId); + setSaveMessage({ type: 'info', text: `"${fieldName}" removed.` }); + // Refresh both farms (total_area updated by DB trigger) and fields list + await loadData(); + const refreshed = await farmApi.getFarmFields(selectedFarmId); + setFields(refreshed || []); + setTimeout(() => setSaveMessage(null), 2500); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Failed to delete field'; + setSaveMessage({ type: 'error', text: msg }); + } finally { + setDeletingFieldId(null); + } + }; + // Fertilizer calc — all values derived from farm area + selected crop const fertFarm = farms.find(f => f.id === fertFarmId) || farms[0]; const soilKey = fertFarm?.soil_type || 'default'; @@ -418,6 +462,11 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => {
{farm.total_area ? `${farm.total_area.toFixed(1)} acres` : 'Area not mapped'} — {farm.irrigation_type || 'Irrigation type not set'} + {farm.id === selectedFarmId && fields.length > 0 && ( + + · {fields.length} field{fields.length !== 1 ? 's' : ''} mapped + + )}
@@ -464,16 +513,20 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { <>
-
Boundary Mapper
+
Field Manager
- Click at least 3 corners on the map to draw your field boundary. Area is calculated automatically. + Draw and name each field boundary. You can add multiple fields per farm.
{hasFarms && ( @@ -489,13 +542,13 @@ const Dashboard: React.FC = ({ activeTab, setActiveTab }) => { {!hasFarms ? (
No farm to map
-
Create a farm first, then draw its boundary here.
+
Create a farm first, then draw its field boundaries here.
) : ( <> - {/* PolygonMapper: do NOT constrain height here — let it render fully */} -
+ {/* ── Polygon drawing tool ── */} +
= ({ activeTab, setActiveTab }) => { ? { lat: selectedFarm.location.latitude, lng: selectedFarm.location.longitude } : { lat: 22.5726, lng: 88.3639 } } - onPolygonComplete={data => setPolygonData(data)} + onPolygonComplete={data => setPendingField(data)} />
- {/* Confirm area panel — shown when polygon data available */} - {polygonData && ( + {/* ── Pending field confirm panel ── */} + {pendingField && (
= ({ activeTab, setActiveTab }) => { boxShadow: '0 2px 12px rgba(16,185,129,0.1)', }}>
-
Field Area
-
- {polygonData.areaAcres.toFixed(2)} - Acres +
+ Ready to save
-
- {polygonData.areaHectares.toFixed(2)} Hectares +
+ {pendingField.fieldName} +
+
+ {pendingField.areaAcres.toFixed(2)} acres  ·  {pendingField.areaHectares.toFixed(2)} ha
)} + + {/* ── Saved fields list ── */} + {fields.length > 0 ? ( +
+
+
+ Mapped Fields +
+
+ {fields.length} field{fields.length !== 1 ? 's' : ''}  ·  {selectedFarm?.total_area?.toFixed(1) ?? '0'} acres total +
+
+ {fields.map((field, idx) => ( +
+
+
+
+ {field.name} +
+
+ {field.area_acres.toFixed(2)} acres + {field.area_hectares ? ` · ${field.area_hectares.toFixed(2)} ha` : ''} +
+
+ +
+ ))} +
+ ) : ( +
+ No fields mapped yet. Draw a boundary above and give it a name to add your first field. +
+ )} )} diff --git a/frontend/src/components/FarmMap.tsx b/frontend/src/components/FarmMap.tsx index 1ccfe45..333dd77 100644 --- a/frontend/src/components/FarmMap.tsx +++ b/frontend/src/components/FarmMap.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Polygon } from 'react-leaflet'; import L from 'leaflet'; import { farmApi } from '../utils/farmApi'; +import { FIELD_COLORS } from '../utils/mapUtils'; import styles from '../styles/Map.module.css'; // Fix for Leaflet marker icons in Next.js @@ -11,11 +12,20 @@ const DefaultIcon = L.icon({ iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], - shadowSize: [41, 41] + shadowSize: [41, 41], }); - L.Marker.prototype.options.icon = DefaultIcon; +interface Field { + id: string; + name: string; + area_acres: number; + area_hectares?: number; + polygon: Array<{ lat: number; lng: number }>; + center_latitude?: number; + center_longitude?: number; +} + interface Farm { id: string; name: string; @@ -25,17 +35,8 @@ interface Farm { state?: string; district?: string; village?: string; - polygon?: Array<{lat: number, lng: number}>; - fields?: Array<{ - id: string; - name: string; - area_acres: number; - area_hectares?: number; - polygon: Array<{lat: number, lng: number}>; - center_latitude?: number; - center_longitude?: number; - created_at?: string; - }>; + /** Legacy single-polygon stored directly on the farm */ + polygon?: Array<{ lat: number; lng: number }>; }; total_area: number; soil_type?: string; @@ -48,20 +49,20 @@ interface FarmMapProps { centerLng?: number; zoom?: number; showAllFarms?: boolean; - showAllFields?: boolean; height?: string; } -const FarmMap: React.FC = ({ - farmId, - centerLat = 20.5937, +const FarmMap: React.FC = ({ + farmId, + centerLat = 20.5937, centerLng = 78.9629, zoom = 5, showAllFarms = false, - showAllFields = false, - height = '400px' + height = '400px', }) => { const [farms, setFarms] = useState([]); + // Map from farm ID → its fields fetched from farm_fields table + const [fieldsByFarm, setFieldsByFarm] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [mapCenter, setMapCenter] = useState<[number, number]>([centerLat, centerLng]); @@ -71,50 +72,66 @@ const FarmMap: React.FC = ({ const loadFarmData = async () => { try { setLoading(true); - + if (farmId) { const farm = await farmApi.getFarm(farmId); - if (farm) { - setFarms([farm]); - if (farm.location?.latitude && farm.location?.longitude) { - setMapCenter([farm.location.latitude, farm.location.longitude]); - setMapZoom(16); - } else if (farm.location?.polygon && farm.location.polygon.length >= 3) { - const polygon = farm.location.polygon; - const centerLat = polygon.reduce((sum: number, p: {lat: number}) => sum + p.lat, 0) / polygon.length; - const centerLng = polygon.reduce((sum: number, p: {lng: number}) => sum + p.lng, 0) / polygon.length; - setMapCenter([centerLat, centerLng]); - setMapZoom(16); - } - } else { - setFarms([]); - setError('Farm not found'); + if (!farm) { setError('Farm not found'); return; } + + setFarms([farm]); + + // Load fields from the dedicated table + const farmFields: Field[] = await farmApi.getFarmFields(farmId).catch(() => []); + setFieldsByFarm({ [farmId]: farmFields }); + + // Set map centre from first field, then legacy polygon centroid, then lat/lng + const firstField = farmFields[0]; + if (firstField?.center_latitude && firstField?.center_longitude) { + setMapCenter([firstField.center_latitude, firstField.center_longitude]); + setMapZoom(16); + } else if (farm.location?.polygon && farm.location.polygon.length >= 3) { + const poly = farm.location.polygon; + setMapCenter([ + poly.reduce((s: number, p: { lat: number }) => s + p.lat, 0) / poly.length, + poly.reduce((s: number, p: { lng: number }) => s + p.lng, 0) / poly.length, + ]); + setMapZoom(16); + } else if (farm.location?.latitude && farm.location?.longitude) { + setMapCenter([farm.location.latitude, farm.location.longitude]); + setMapZoom(16); } } else if (showAllFarms) { - const allFarms = await farmApi.getFarms(); - const farmsWithCoords = allFarms.filter( - (farm: Farm) => (farm.location?.latitude && farm.location?.longitude) || - (farm.location?.polygon && farm.location.polygon.length >= 3) + const allFarms: Farm[] = await farmApi.getFarms(); + const farmsWithData = allFarms.filter( + f => + (f.location?.latitude && f.location?.longitude) || + (f.location?.polygon?.length ?? 0) >= 3 ); - - setFarms(farmsWithCoords); - - if (farmsWithCoords.length > 0) { - const firstFarm = farmsWithCoords[0]; - if (firstFarm.location?.latitude && firstFarm.location?.longitude) { - setMapCenter([firstFarm.location.latitude, firstFarm.location.longitude]); - } else if (firstFarm.location?.polygon && firstFarm.location.polygon.length >= 3) { - const polygon = firstFarm.location.polygon; - const centerLat = polygon.reduce((sum: number, p: {lat: number}) => sum + p.lat, 0) / polygon.length; - const centerLng = polygon.reduce((sum: number, p: {lng: number}) => sum + p.lng, 0) / polygon.length; - setMapCenter([centerLat, centerLng]); + setFarms(farmsWithData); + + // Load fields for each farm in parallel + const entries = await Promise.all( + farmsWithData.map(async f => { + const ff: Field[] = await farmApi.getFarmFields(f.id).catch(() => []); + return [f.id, ff] as [string, Field[]]; + }) + ); + setFieldsByFarm(Object.fromEntries(entries)); + + if (farmsWithData.length > 0) { + const first = farmsWithData[0]; + const firstField = entries[0]?.[1]?.[0]; + if (firstField?.center_latitude && firstField?.center_longitude) { + setMapCenter([firstField.center_latitude, firstField.center_longitude]); + } else if (first.location?.latitude && first.location?.longitude) { + setMapCenter([first.location.latitude, first.location.longitude]); } setMapZoom(12); } } - } catch (err: any) { - console.error('Error loading farm data:', err); - setError(err.message || 'Failed to load farm data'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Failed to load farm data'; + console.error('FarmMap load error:', err); + setError(msg); } finally { setLoading(false); } @@ -134,17 +151,17 @@ const FarmMap: React.FC = ({ if (farms.length === 0) { return (
-

- No farms with location data available.
- Use "Draw Field" to add field boundaries. +

+ No farms with location data available.
+ Use "Field Manager" to add field boundaries.

); } - const heightClass = - height === '100%' ? styles.mapHeightFull : - height === '250px' ? styles.mapHeightSmall : + const heightClass = + height === '100%' ? styles.mapHeightFull : + height === '250px' ? styles.mapHeightSmall : styles.mapHeight; return ( @@ -158,70 +175,98 @@ const FarmMap: React.FC = ({ attribution='© OpenStreetMap' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> + {farms.map(farm => { - const hasPolygon = farm.location?.polygon && farm.location.polygon.length >= 3; - const hasFields = farm.location?.fields && farm.location.fields.length > 0; - const lat = farm.location?.latitude; - const lng = farm.location?.longitude; - - // Derive center from polygon centroid if GPS lat/lng not set - const polyCenter = hasPolygon ? { - lat: farm.location!.polygon!.reduce((s: number, p: {lat: number}) => s + p.lat, 0) / farm.location!.polygon!.length, - lng: farm.location!.polygon!.reduce((s: number, p: {lng: number}) => s + p.lng, 0) / farm.location!.polygon!.length, - } : null; - - const centerLat = lat ?? polyCenter?.lat ?? farm.location?.fields?.[0]?.center_latitude; - const centerLng = lng ?? polyCenter?.lng ?? farm.location?.fields?.[0]?.center_longitude; - - - if (!centerLat || !centerLng) return null; - + const fields: Field[] = fieldsByFarm[farm.id] ?? []; + const hasFields = fields.length > 0; + const hasLegacyPolygon = + !hasFields && (farm.location?.polygon?.length ?? 0) >= 3; + + // Marker position: first field centre → legacy polygon centroid → lat/lng + const markerLat = + fields[0]?.center_latitude ?? + farm.location?.latitude ?? + (hasLegacyPolygon + ? farm.location!.polygon!.reduce((s: number, p: { lat: number }) => s + p.lat, 0) / farm.location!.polygon!.length + : undefined); + const markerLng = + fields[0]?.center_longitude ?? + farm.location?.longitude ?? + (hasLegacyPolygon + ? farm.location!.polygon!.reduce((s: number, p: { lng: number }) => s + p.lng, 0) / farm.location!.polygon!.length + : undefined); + return ( - {showAllFields && hasFields ? ( - farm.location.fields!.map((field, idx) => ( - field.polygon && field.polygon.length >= 3 && ( - [p.lat, p.lng])} - pathOptions={{ color: '#8b5cf6', fillColor: '#8b5cf6', fillOpacity: 0.3, weight: 3 }} + {/* Named fields from farm_fields table — each gets its own colour */} + {hasFields && + fields.map((field, idx) => { + if (!field.polygon || field.polygon.length < 3) return null; + const color = FIELD_COLORS[idx % FIELD_COLORS.length]; + return ( + [p.lat, p.lng] as [number, number])} + pathOptions={{ color, fillColor: color, fillOpacity: 0.25, weight: 2.5 }} > -
-

{field.name}

-

Area: {field.area_acres} acres

- {field.area_hectares &&

Area: {field.area_hectares} ha

} +
+
+ {field.name} +
+
+ {field.area_acres.toFixed(2)} acres + {field.area_hectares ? ` · ${field.area_hectares.toFixed(2)} ha` : ''} +
+
+ {farm.name} +
- ) - )) - ) : hasPolygon && ( - [p.lat, p.lng])} - pathOptions={{ color: '#16a34a', fillColor: '#16a34a', fillOpacity: 0.3, weight: 3 }} - /> + ); + })} + + {/* Legacy single polygon — only shown when no named fields exist */} + {hasLegacyPolygon && ( + [p.lat, p.lng] as [number, number])} + pathOptions={{ color: '#16a34a', fillColor: '#16a34a', fillOpacity: 0.25, weight: 2.5 }} + > + +
{farm.name}
+
+
+ )} + + {/* Farm marker */} + {markerLat !== undefined && markerLng !== undefined && ( + + +
+
{farm.name}
+
+ {farm.total_area.toFixed(1)} acres total +
+ {hasFields && ( +
+ {fields.length} field{fields.length !== 1 ? 's' : ''} mapped +
+ )} + {farm.soil_type && ( +
Soil: {farm.soil_type}
+ )} + {(farm.location?.village || farm.location?.district || farm.location?.state) && ( +
+ {[farm.location?.village, farm.location?.district, farm.location?.state] + .filter(Boolean) + .join(', ')} +
+ )} +
+
+
)} - - -
-

{farm.name}

-

Area: {farm.total_area} acres

- {farm.soil_type &&

Soil: {farm.soil_type}

} - {farm.irrigation_type &&

Irri: {farm.irrigation_type}

} - {hasPolygon && ( -

- ✓ Field boundary mapped ({farm.location.polygon!.length} points) -

- )} - {(farm.location?.village || farm.location?.district || farm.location?.state) && ( -

- {[farm.location?.village, farm.location?.district, farm.location?.state].filter(Boolean).join(', ')} -

- )} -
-
-
); })} @@ -230,4 +275,4 @@ const FarmMap: React.FC = ({ ); }; -export default FarmMap; \ No newline at end of file +export default FarmMap; diff --git a/frontend/src/components/map/PolygonMapper.tsx b/frontend/src/components/map/PolygonMapper.tsx index 8285a2d..0f11ea7 100644 --- a/frontend/src/components/map/PolygonMapper.tsx +++ b/frontend/src/components/map/PolygonMapper.tsx @@ -1,13 +1,12 @@ -'use client'; // if you are using Next.js App Router +'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; import { usePolygonArea } from '../../hooks/usePolygonArea'; import SearchBox from './SearchBox'; import type { LatLng } from '../../utils/geoUtils'; import WeatherStatusBadge from './WeatherStatusBadge'; // Leaflet must be loaded client-side only (it accesses window) -// So we lazy-load the inner map component const MapInner = dynamic(() => import('./MapInner'), { ssr: false, loading: () => ( @@ -21,39 +20,38 @@ const MapInner = dynamic(() => import('./MapInner'), { ), }); +interface PolygonCompleteData { + fieldName: string; + coordinates: LatLng[]; + areaAcres: number; + areaHectares: number; + centerLat: number; + centerLng: number; +} + interface PolygonMapperProps { - onPolygonComplete: (data: { - coordinates: LatLng[]; - areaAcres: number; - areaHectares: number; - centerLat: number; - centerLng: number; - }) => void; + onPolygonComplete: (data: PolygonCompleteData) => void; initialCenter?: LatLng; - showInstructions?: boolean; } -export default function PolygonMapper({ onPolygonComplete, initialCenter, showInstructions = true }: PolygonMapperProps) { +export default function PolygonMapper({ + onPolygonComplete, + initialCenter, +}: PolygonMapperProps) { const { points, area, center, isComplete, addPoint, removeLastPoint, resetPolygon } = usePolygonArea(); const [mapCenter, setMapCenter] = useState(initialCenter || { lat: 22.5726, lng: 88.3639 }); const [mapZoom, setMapZoom] = useState(13); - const [locationLoading, setLocationLoading] = useState(false); + const [fieldName, setFieldName] = useState(''); + const [nameError, setNameError] = useState(''); useEffect(() => { if (!initialCenter && navigator.geolocation) { - setLocationLoading(true); navigator.geolocation.getCurrentPosition( (position) => { - setMapCenter({ - lat: position.coords.latitude, - lng: position.coords.longitude - }); + setMapCenter({ lat: position.coords.latitude, lng: position.coords.longitude }); setMapZoom(15); - setLocationLoading(false); }, - () => { - setLocationLoading(false); - } + () => { /* geolocation denied — keep default centre */ } ); } else if (initialCenter) { setMapCenter(initialCenter); @@ -68,13 +66,23 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn const handleConfirm = () => { if (!isComplete) return; + const trimmed = fieldName.trim(); + if (!trimmed) { + setNameError('Give this field a name before saving.'); + return; + } + setNameError(''); onPolygonComplete({ + fieldName: trimmed, coordinates: points, areaAcres: area.acres, areaHectares: area.hectares, centerLat: center.lat, centerLng: center.lng, }); + // Reset for the next field + resetPolygon(); + setFieldName(''); }; return ( @@ -83,14 +91,50 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn {/* Header */}

- Draw your field boundary + Draw a field boundary

- Click 4 corners to mark your field boundary. + Name the field, then click 4 corners on the map to draw its boundary.

- {/* Search bar (floats above the map visually) */} + {/* Field name input */} +
+ + { setFieldName(e.target.value); if (nameError) setNameError(''); }} + placeholder="e.g. Wheat Field, North Paddy, Block A" + maxLength={60} + style={{ + width: '100%', + padding: '10px 14px', + border: `1.5px solid ${nameError ? '#fca5a5' : 'var(--color-border)'}`, + borderRadius: '10px', + fontSize: '14px', + color: 'var(--color-text-primary)', + background: 'white', + fontFamily: 'inherit', + boxSizing: 'border-box', + outline: 'none', + transition: 'border-color 0.15s', + }} + onFocus={e => { e.currentTarget.style.borderColor = '#10b981'; }} + onBlur={e => { e.currentTarget.style.borderColor = nameError ? '#fca5a5' : 'var(--color-border)'; }} + /> + {nameError && ( +

{nameError}

+ )} +
+ + {/* Search bar */} {/* Map */} @@ -105,19 +149,19 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn onMapClick={addPoint} /> - {/* Instructions overlay (top-left of map) */} + {/* How-to overlay */}
- How to use:
- 1. Search your village
- 2. Click corners of your field
- 3. Drop 3+ pins to form a polygon + How to use:
+ 1. Name your field above
+ 2. Search your village
+ 3. Click 4 corners to mark boundary
@@ -127,7 +171,7 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn background: isComplete ? '#f0fdf4' : '#f9fafb', border: `1px solid ${isComplete ? '#86efac' : '#e5e7eb'}`, borderRadius: '10px', padding: '12px 16px', - transition: 'all 0.3s ease' + transition: 'all 0.3s ease', }}> {/* Pin count */}
@@ -135,27 +179,22 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn background: '#16a34a', color: 'white', borderRadius: '50%', width: '20px', height: '20px', fontSize: '11px', display: 'flex', alignItems: 'center', justifyContent: 'center', - fontWeight: 600 + fontWeight: 600, }}>{points.length} {points.length === 1 ? 'pin dropped' : 'pins dropped'}
- {/* Area display */} {isComplete && ( <> -
+
- - {area.acres} - + {area.acres} acres
- - {area.hectares} - + {area.hectares} hectares
@@ -168,7 +207,7 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn {!isComplete && points.length > 0 && ( - Drop {4 - points.length} more {4 - points.length === 1 ? 'pin' : 'pins'} to complete boundary + Drop {Math.max(0, 4 - points.length)} more {(4 - points.length) === 1 ? 'pin' : 'pins'} to complete boundary )} @@ -178,8 +217,7 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn )} - {/* Spacer */} -
+
{/* Action buttons */}
@@ -189,7 +227,7 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn style={{ padding: '6px 12px', border: '1px solid #d1d5db', borderRadius: '6px', background: 'white', - fontSize: '12px', cursor: 'pointer', color: '#374151' + fontSize: '12px', cursor: 'pointer', color: '#374151', }} > Undo last pin @@ -201,7 +239,7 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn style={{ padding: '6px 12px', border: '1px solid #fca5a5', borderRadius: '6px', background: '#fef2f2', - fontSize: '12px', cursor: 'pointer', color: '#dc2626' + fontSize: '12px', cursor: 'pointer', color: '#dc2626', }} > Reset @@ -218,14 +256,14 @@ export default function PolygonMapper({ onPolygonComplete, initialCenter, showIn fontSize: '13px', cursor: 'pointer', color: 'white', - fontWeight: 500 + fontWeight: 500, }} > - Confirm field boundary → + Add field → )}
); -} \ No newline at end of file +} diff --git a/frontend/src/utils/mapUtils.ts b/frontend/src/utils/mapUtils.ts new file mode 100644 index 0000000..a46fa09 --- /dev/null +++ b/frontend/src/utils/mapUtils.ts @@ -0,0 +1,21 @@ +/** + * Shared map utilities + * + * Constants and helpers used by both the Dashboard (field list swatches) + * and FarmMap (polygon rendering) so the colours stay in sync. + */ + +/** + * Colour palette for named field polygons and swatches. + * Index cycles when a farm has more fields than palette entries. + */ +export const FIELD_COLORS: readonly string[] = [ + '#10b981', // emerald + '#f97316', // orange + '#6366f1', // indigo + '#eab308', // yellow + '#ec4899', // pink + '#06b6d4', // cyan + '#84cc16', // lime + '#a855f7', // purple +] as const; diff --git a/package-lock.json b/package-lock.json index aed6693..4eaed6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "Agronavis", "version": "2.0.0", "devDependencies": { + "concurrently": "^8.2.2", + "supabase": "^2.105.0" "@commitlint/cli": "^21.0.2", "@commitlint/config-conventional": "^21.0.2", "concurrently": "^8.2.2", @@ -49,6 +51,117 @@ "node": ">=6.9.0" } }, + "node_modules/@supabase/cli-darwin-arm64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.105.0.tgz", + "integrity": "sha512-ptlLrggNCq7dndvY7ce0MIvCZRnAYXeyKC0H7c4DqQmCbOPZgSwD5a4E3RPrZS6TLeJPG7XhpuJarAU5PTf/9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-darwin-x64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.105.0.tgz", + "integrity": "sha512-kJDYyy3UkXd4hDzo+duOzUk8yDqLEit8d/clBCiXNQFSJf96QCnG2iyrzT4dCDUk8UB3+g9xL1mH2EEuPHj7oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-linux-arm64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.105.0.tgz", + "integrity": "sha512-cdHgfIFElYkAjrp0aJPlQPTqQ9doSxB4qRswlGNsSYaLlK5Ns78rXsB7G71RRnyrzb41C183osRhVENWVrnK/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-arm64-musl": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.105.0.tgz", + "integrity": "sha512-45vxbXRwe/JUAjyvQ8oKO2o44IK3GunlVqeIWGzcDgaeMg5XO4x0i9+NGPvOvvhyMB4bgJ3cMxh/ovl1AM0GZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.105.0.tgz", + "integrity": "sha512-rj1iU0h4EJaanx72eJtipiXczAY3gug9qxo62MYUZg2X1aaegFtyMS83fmjC7YlduW2Cu+zvgWoyTlA6R5Ndzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64-musl": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.105.0.tgz", + "integrity": "sha512-VnHn1NPn9Ov81BXFOx6FpHxbBT5e3ObaUxO7DR+LDGjh2wfMUj4E6Y863w+r0HJk0hi0mRH7u0U0dOrbBp4E+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-windows-arm64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.105.0.tgz", + "integrity": "sha512-b5xQ5dVkARZTBiSwLpPbwffRHvVDcO3ZvAtzz54J44KQdLD0TSrgoXZJBrI4Y3ggmG70EiITKPL8+rT6ptjXsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@supabase/cli-windows-x64": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.105.0.tgz", + "integrity": "sha512-QmfqDQJ58ba/9+vzM8HJQsa/rY6OdtaaaAmPKwCMd8JUQ7758p8sydYe1vXTb4CCPdvk9qW2d/Dh54ucH4SPwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] "node_modules/@commitlint/cli": { "version": "21.0.2", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.0.2.tgz", @@ -1234,6 +1347,26 @@ "node": ">=8" } }, + "node_modules/supabase": { + "version": "2.105.0", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.105.0.tgz", + "integrity": "sha512-UB2aFLYAVujTQsZ9l+aCbDfLNaZApZucByRNP/1j0L1pXXzFhSgEyZSrvHSUO5LIvOb09AGHWishL/usVTuHTg==", + "dev": true, + "license": "MIT", + "bin": { + "supabase": "dist/supabase.js" + }, + "optionalDependencies": { + "@supabase/cli-darwin-arm64": "2.105.0", + "@supabase/cli-darwin-x64": "2.105.0", + "@supabase/cli-linux-arm64": "2.105.0", + "@supabase/cli-linux-arm64-musl": "2.105.0", + "@supabase/cli-linux-x64": "2.105.0", + "@supabase/cli-linux-x64-musl": "2.105.0", + "@supabase/cli-windows-arm64": "2.105.0", + "@supabase/cli-windows-x64": "2.105.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", diff --git a/package.json b/package.json index 656edad..9b276de 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "concurrently --names \"API,WEB\" --prefix-colors \"green,cyan\" \"npm run dev:api\" \"npm run dev:web\"", - "dev:api": "cd backend && uvicorn main:app --reload --port 8000", + "dev:api": "backend\\venv\\Scripts\\uvicorn.exe main:app --reload --port 8000 --app-dir backend", "dev:web": "cd frontend && npm run dev", "install:frontend": "npm install --prefix frontend", "build": "npm run build --prefix frontend", @@ -12,6 +12,7 @@ "prepare": "husky" }, "devDependencies": { + "supabase": "^2.105.0", "@commitlint/cli": "^21.0.2", "@commitlint/config-conventional": "^21.0.2", "concurrently": "^8.2.2",