diff --git a/Makefile b/Makefile index 50889fa..4602698 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install run test fuzz lint format audit a11y ci migrate schema-dump backup restore docker-up docker-down clean +.PHONY: help install run demo-readings test fuzz lint format audit a11y ci migrate schema-dump backup restore docker-up docker-down clean help: ## Show this help @egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' @@ -10,6 +10,9 @@ install: ## Install python dependencies run: ## Run the FastAPI application locally uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +demo-readings: ## Demo öncesi: sensörlere son 48h taze toprak nemi okuması (DB sıfırlamadan) + PYTHONPATH=. python scripts/seed_demo_readings.py + test: ## Run the test suite with coverage (Schemathesis fuzz hariç — `make fuzz`) SKIP_SCHEMATHESIS=1 pytest tests/ --cov=app --cov-report=term-missing diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 6dcc2c4..dafec75 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -44,6 +44,7 @@ from app.schemas.schemas import ( DashboardLastDisease, DashboardLastIrrigation, + DashboardMoisturePoint, DashboardOpenAlerts, DashboardSoilMoisture, DashboardSummaryResponse, @@ -145,6 +146,28 @@ def get_dashboard_summary( status=_classify_moisture(avg_moisture), ) + # Toprak nemi trendi (son 24 saat, saatlik ortalama) — dashboard grafiği için. + # DB-agnostik: ham okumalar Python'da saatlik bucket'lanır (~192 satır, hafif). + trend_q = ( + db.query(SoilMoistureReading.reading_timestamp, SoilMoistureReading.moisture_percent) + .join(Sensor, SoilMoistureReading.sensor_id == Sensor.id) + .join(Field, Sensor.field_id == Field.id) + .join(Farm, Field.farm_id == Farm.id) + .filter(SoilMoistureReading.reading_timestamp >= since) + ) + if not is_system: + trend_q = trend_q.filter(Farm.user_id == current_user.id) + hourly_buckets: dict[datetime, list[float]] = {} + for ts, moisture in trend_q.all(): + if ts is None or moisture is None: + continue + bucket = ts.replace(minute=0, second=0, microsecond=0) + hourly_buckets.setdefault(bucket, []).append(moisture) + moisture_trend = [ + DashboardMoisturePoint(hour=bucket, moisture_percent=round(sum(vals) / len(vals), 1)) + for bucket, vals in sorted(hourly_buckets.items()) + ] + # ─── 2. Son sulama ─────────────────────────────────────────── irrigation_q = ( db.query(IrrigationSchedule, Field.name) @@ -224,6 +247,7 @@ def get_dashboard_summary( field_count=field_count, sensor_count=sensor_count, soil_moisture_today=soil_moisture, + soil_moisture_trend=moisture_trend, last_irrigation=last_irrigation, open_alerts=open_alerts, last_disease=last_disease, diff --git a/app/schemas/dashboard.py b/app/schemas/dashboard.py index 55b10d7..daa921d 100644 --- a/app/schemas/dashboard.py +++ b/app/schemas/dashboard.py @@ -22,6 +22,13 @@ class DashboardSoilMoisture(BaseModel): status: str = "no_data" # 'dry' | 'optimal' | 'wet' | 'no_data' +class DashboardMoisturePoint(BaseModel): + """Saatlik ortalama toprak nemi — dashboard 'Toprak Nemi Trendi' grafiği için.""" + + hour: UtcDateTime + moisture_percent: float + + class DashboardLastIrrigation(BaseModel): """En son planlanan/gerçekleşen sulama kaydı.""" @@ -69,6 +76,7 @@ class DashboardSummaryResponse(BaseModel): field_count: int sensor_count: int soil_moisture_today: DashboardSoilMoisture + soil_moisture_trend: list[DashboardMoisturePoint] = Field(default_factory=list) last_irrigation: DashboardLastIrrigation open_alerts: DashboardOpenAlerts last_disease: DashboardLastDisease diff --git a/docs/demo_script.md b/docs/demo_script.md index d10259f..ad121bf 100644 --- a/docs/demo_script.md +++ b/docs/demo_script.md @@ -18,6 +18,8 @@ rm -f sfdap_dev.db && python database/seed_data.py make run # uvicorn → http://localhost:8000 ``` +> 💡 **Alternatif** (DB sıfırlamadan): mevcut demo verisini koruyup okumaları tazelemek → `make demo-readings`. + Tarayıcıda aç: **http://localhost:8000/dashboard/** (+ opsiyonel `/docs` Swagger). **Demo hesapları:** diff --git a/frontend/index.html b/frontend/index.html index 5b9a183..62d5f3e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -899,7 +899,7 @@
Güven: %${_fmtNumber(data.confidence_score * 100, 0)} · Şiddet: ${_escAttr(sev)}
+Güven: ${data.confidence_score != null ? '%' + _fmtNumber(data.confidence_score * 100, 0) : '—'} · Şiddet: ${_escAttr(severityLabel(sev))}
${r.recommendation}
Gübreleme takvimi oluşturulamadı.
'; + showToast('Gübreleme takvimi alınamadı', 'error'); } } @@ -368,9 +373,9 @@ export async function loadPlants() { html += `Açık uyarı yok ✅
'; return ` @@ -136,9 +137,9 @@ export function renderPlantResult(data) { const sevColor = sev === 'high' ? '#ef4444' : sev === 'medium' ? '#f59e0b' : sev === 'low' ? '#eab308' : '#22c55e'; const conf = (data.confidence_score * 100).toFixed(1); let html = `Güven: %${conf}
-Şiddet: ${sev}
+Şiddet: ${_escAttr(severityLabel(sev))}
Model: ${data.model_version}