From bfa718d7a6ff4165381c722250fac7cd668a9a67 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:17:37 +0300
Subject: [PATCH 1/8] =?UTF-8?q?fix(frontend):=20demo-kritik=20d=C3=BCzeltm?=
=?UTF-8?q?eler=20=E2=80=94=20i18n=20enum=20etiketleri=20+=20XSS=20escape?=
=?UTF-8?q?=20+=20leaf=20severity=20+=20filiz=20polling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 🌐 i18n: backend enum'larını Türkçe etikete çeviren labels.js (severity / alert-type /
irrigation-status / diagnosis) → alerts, dashboard, monitoring, fields'te uygulandı.
Artık "critical / low_moisture / completed / healthy" yerine
"Kritik / Düşük nem / Tamamlandı / Sağlıklı".
- 🔒 XSS: _escAttr kırık regex (/<>/g → /
---
frontend/src/lib/filiz.js | 2 +-
frontend/src/lib/labels.js | 26 ++++++++++++++++++++++++++
frontend/src/lib/pages/alerts.js | 7 ++++---
frontend/src/lib/pages/dashboard.js | 5 +++--
frontend/src/lib/pages/fields.js | 7 ++++---
frontend/src/lib/pages/monitoring.js | 7 ++++---
frontend/src/lib/utils.js | 2 +-
7 files changed, 43 insertions(+), 13 deletions(-)
create mode 100644 frontend/src/lib/labels.js
diff --git a/frontend/src/lib/filiz.js b/frontend/src/lib/filiz.js
index 6ff8adf..1fa5c2f 100644
--- a/frontend/src/lib/filiz.js
+++ b/frontend/src/lib/filiz.js
@@ -423,7 +423,7 @@ export function initFiliz() {
setFilizMood(isLateNight ? 'sleepy' : 'happy');
// Alert listesini balon için arka planda topla (mood'u etkilemez)
- if (getApiOnline()) {
+ if (getApiOnline() && getCurrentUser()) {
try {
const alerts = await api('/api/alerts/?severity=critical&is_resolved=false&limit=10');
filizCriticalAlerts = alerts || [];
diff --git a/frontend/src/lib/labels.js b/frontend/src/lib/labels.js
new file mode 100644
index 0000000..c638207
--- /dev/null
+++ b/frontend/src/lib/labels.js
@@ -0,0 +1,26 @@
+/* ════════════════════════════════════════════════════════════════
+ i18n — backend enum değerlerini Türkçe etikete çevirir.
+ Bilinmeyen değer → değerin kendisi (fallback); null/boş → '—'.
+ ════════════════════════════════════════════════════════════════ */
+const _map = (m) => (v) => (v == null || v === '' ? '—' : (m[String(v).toLowerCase()] ?? String(v)));
+
+// Uyarı + hastalık şiddeti (low/medium/critical + none/low/medium/high)
+export const severityLabel = _map({
+ none: 'Yok', low: 'Düşük', medium: 'Orta', high: 'Yüksek', critical: 'Kritik',
+});
+
+// Uyarı tipi
+export const alertTypeLabel = _map({
+ low_moisture: 'Düşük nem', disease_reminder: 'Hastalık hatırlatması', model_drift: 'Model sapması',
+});
+
+// Sulama durumu
+export const irrigationStatusLabel = _map({
+ pending: 'Beklemede', completed: 'Tamamlandı', cancelled: 'İptal', scheduled: 'Planlandı',
+});
+
+// Hastalık tanısı (ML sınıfları)
+export const diagnosisLabel = _map({
+ healthy: 'Sağlıklı', leaf_spot: 'Yaprak lekesi', powdery_mildew: 'Külleme', rust: 'Pas',
+ blight: 'Yanıklık', mosaic_virus: 'Mozaik virüsü', bacterial_wilt: 'Bakteriyel solgunluk', anthracnose: 'Antraknoz',
+});
diff --git a/frontend/src/lib/pages/alerts.js b/frontend/src/lib/pages/alerts.js
index 32fb463..ee1a07a 100644
--- a/frontend/src/lib/pages/alerts.js
+++ b/frontend/src/lib/pages/alerts.js
@@ -9,6 +9,7 @@
import { _escAttr, showToast } from "../utils.js";
import { api, apiAuth, API_BASE } from "../api.js";
import { _skeletonCards, _skeletonBlock, _setBusy } from "../skeleton.js";
+import { severityLabel, alertTypeLabel } from "../labels.js";
// ─── ALERTS ───────────────────────────────────────────────────
export async function loadAlerts() {
@@ -57,8 +58,8 @@ export async function loadAlerts() {
: ``;
html += `
| ${date} |
- ${a.severity} |
- ${a.alert_type} |
+ ${severityLabel(a.severity)} |
+ ${alertTypeLabel(a.alert_type)} |
${a.message} |
${status} |
`;
@@ -115,7 +116,7 @@ export async function refreshBell() {
`).join('');
diff --git a/frontend/src/lib/pages/dashboard.js b/frontend/src/lib/pages/dashboard.js
index e9e7680..8800a27 100644
--- a/frontend/src/lib/pages/dashboard.js
+++ b/frontend/src/lib/pages/dashboard.js
@@ -9,6 +9,7 @@
import { _fmtDate, _fmtNumber, _escAttr, showToast, updateStatus, _STATUS_EMOJI, _STATUS_LABEL } from "../utils.js";
import { api, apiAuth, getAuthToken } from "../api.js";
import { _skeletonCards, _setBusy } from "../skeleton.js";
+import { irrigationStatusLabel, diagnosisLabel } from "../labels.js";
import { renderMoistureChart, renderTempHumChart, renderPrecipChart } from "../charts.js";
import { getCurrentUser, getApiOnline, setApiOnline } from "../session.js";
import { _getRoleTips } from "../filiz.js";
@@ -41,7 +42,7 @@ function _renderSummaryCards(summary) {
? `${cnt} ${label}`
: '';
}).join(' ');
- const diseaseDx = disease.diagnosis || '—';
+ const diseaseDx = diagnosisLabel(disease.diagnosis);
const diseaseDetail = disease.diagnosis
? `${disease.field_name || 'Tarla'} · ${_fmtDate(disease.captured_at)} · güven %${_fmtNumber((disease.confidence_score || 0) * 100, 0)}`
: 'Henüz yaprak görseli analiz edilmedi';
@@ -66,7 +67,7 @@ function _renderSummaryCards(summary) {
${irrAmount}
${_escAttr(irr.field_name || '—')} · ${_fmtDate(irr.scheduled_date)}
- ${_escAttr(irr.status || (irr.irrigation_id ? '—' : 'Sulama kaydı yok'))}
+ ${_escAttr(irr.status ? irrigationStatusLabel(irr.status) : (irr.irrigation_id ? '—' : 'Sulama kaydı yok'))}
${scopeNote}
diff --git a/frontend/src/lib/pages/fields.js b/frontend/src/lib/pages/fields.js
index 3a30d9e..d79b017 100644
--- a/frontend/src/lib/pages/fields.js
+++ b/frontend/src/lib/pages/fields.js
@@ -9,6 +9,7 @@
import { _fmtNumber, _escAttr, showToast } from "../utils.js";
import { apiAuth, getAuthToken, API_BASE } from "../api.js";
import { _skeletonBlock, _setBusy } from "../skeleton.js";
+import { diagnosisLabel, severityLabel } from "../labels.js";
import { charts, chartTick, chartLegend, chartGrid } from "../charts.js";
import { renderFieldDetail } from "../render.js";
import { pageTitles } from "../router.js";
@@ -321,11 +322,11 @@ export async function analyzeFieldLeaf() {
}
const data = await resp.json();
const sev = data.severity || 'none';
- const sevColor = sev === 'severe' ? '#ef4444' : sev === 'moderate' ? '#f97316' : sev === 'mild' ? '#eab308' : '#22c55e';
+ const sevColor = sev === 'high' ? '#ef4444' : sev === 'medium' ? '#f97316' : sev === 'low' ? '#eab308' : '#22c55e';
const box = document.getElementById('fieldLeafResult');
box.innerHTML = `
-
🧪 ${_escAttr(data.diagnosis)}
-
Güven: %${_fmtNumber(data.confidence_score * 100, 0)} · Şiddet: ${_escAttr(sev)}
+
🧪 ${_escAttr(diagnosisLabel(data.diagnosis))}
+
Güven: ${data.confidence_score != null ? '%' + _fmtNumber(data.confidence_score * 100, 0) : '—'} · Şiddet: ${_escAttr(severityLabel(sev))}
`;
box.style.display = 'block';
showToast('Analiz tamamlandı ✅', 'success');
diff --git a/frontend/src/lib/pages/monitoring.js b/frontend/src/lib/pages/monitoring.js
index c6b6d81..556de19 100644
--- a/frontend/src/lib/pages/monitoring.js
+++ b/frontend/src/lib/pages/monitoring.js
@@ -9,6 +9,7 @@
import { _escAttr, showToast } from "../utils.js";
import { api, apiAuth, getAuthToken, API_BASE, _authHeaders } from "../api.js";
import { _skeletonCards, _skeletonRows, _skeletonBlock, _setBusy } from "../skeleton.js";
+import { irrigationStatusLabel, diagnosisLabel, severityLabel } from "../labels.js";
import { charts, renderSensorTypeChart, renderFarmTempChart, renderIrrigationStatusChart,
renderNpkRadarChart, renderDailyTrendChart, renderSensorStatsChart,
chartTick, chartLegend, chartGrid } from "../charts.js";
@@ -155,7 +156,7 @@ export async function loadIrrigation(page = 1) {
| Tarla #${s.field_id} | ${new Date(s.scheduled_date).toLocaleDateString('tr')} |
${s.duration_min || '—'} dk | ${s.water_amount_liters?.toFixed(0) || '—'} |
- ${s.status} |
+ ${irrigationStatusLabel(s.status)} |
`).join('');
_setBusy('irrigationTable', false);
@@ -368,9 +369,9 @@ export async function loadPlants() {
html += `
| ${date} |
#${r.field_id} |
- ${r.diagnosis || '—'} |
+ ${diagnosisLabel(r.diagnosis)} |
${conf} |
- ${sev} |
+ ${severityLabel(sev)} |
`;
}
html += '';
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
index bbb208b..0897d80 100644
--- a/frontend/src/lib/utils.js
+++ b/frontend/src/lib/utils.js
@@ -24,7 +24,7 @@ export function _fmtNumber(v, decimals = 1) {
}
export function _escAttr(s) {
- return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(/<>/g, '<').replace(/>/g, '>');
+ return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
}
// ─── Toast Notifications ──────────────────────────────────────
From a2b7fc5cdc3cd562153ea505b98eaabba8160e10 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:26:50 +0300
Subject: [PATCH 2/8] =?UTF-8?q?chore(demo):=20seed=5Fdemo=5Freadings=20?=
=?UTF-8?q?=E2=80=94=20mevcut=20sens=C3=B6rlere=20son=2048h=20taze=20topra?=
=?UTF-8?q?k=20nemi=20okumas=C4=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Dashboard "son 24 saat" penceresi kullanıyor; seed okumaları eskiyince (>24h)
"Bugün toprak nemi: Veri yok" görünüyordu. Bu script demo öncesi çalıştırılır,
idempotent (her sensör için son 48h yeniden yazılır → dup birikmez).
Sonuç (sfdap_dev.db): 8 sensör × 24h = 192 taze okuma · ort %49.8.
Dashboard kartı "Veri yok" → "%49,8 👌 Uygun · 192 okuma" oldu.
Kullanım: PYTHONPATH=. .venv/bin/python scripts/seed_demo_readings.py
Co-Authored-By: Claude Opus 4.8
---
scripts/seed_demo_readings.py | 58 +++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 scripts/seed_demo_readings.py
diff --git a/scripts/seed_demo_readings.py b/scripts/seed_demo_readings.py
new file mode 100644
index 0000000..f31ddc8
--- /dev/null
+++ b/scripts/seed_demo_readings.py
@@ -0,0 +1,58 @@
+"""Demo hazırlık — mevcut sensörlere son 48 saatlik TAZE toprak nemi okuması ekler.
+
+Neden gerekli: dashboard 'son 24 saat' penceresi kullanır; seed okumaları zamanla
+eskir (>24h) ve 'Veri yok' görünür. Bu script demo ÖNCESİ çalıştırılır.
+Idempotent: her sensör için son 48h yeniden yazılır (dup birikmez).
+
+Kullanım: PYTHONPATH=. .venv/bin/python scripts/seed_demo_readings.py
+"""
+
+import math
+from datetime import UTC, datetime, timedelta
+
+from app.database import SessionLocal
+from app.models.models import Sensor, SoilMoistureReading
+
+HOURS = 48
+
+
+def seed_readings() -> None:
+ db = SessionLocal()
+ try:
+ sensors = db.query(Sensor).all()
+ if not sensors:
+ print("⚠️ Sensör yok — önce seed_data.py çalıştır.")
+ return
+ now = datetime.now(UTC)
+ cutoff = now - timedelta(hours=HOURS)
+ added = 0
+ for i, sensor in enumerate(sensors):
+ # idempotent: son 48h'ı temizle, yeniden yaz
+ db.query(SoilMoistureReading).filter(
+ SoilMoistureReading.sensor_id == sensor.id,
+ SoilMoistureReading.reading_timestamp >= cutoff,
+ ).delete(synchronize_session=False)
+ base = 45.0 + (i % 5) * 3.0 # sensöre göre 45-57 taban nem
+ for h in range(HOURS):
+ ts = now - timedelta(hours=h)
+ diurnal = 8.0 * math.sin((ts.hour / 24) * 2 * math.pi) # gün/gece dalgası
+ noise = (h * 7) % 5 - 2 # -2..+2 deterministik
+ moisture = max(15.0, min(90.0, base + diurnal + noise))
+ temp = 18.0 + 5.0 * math.sin(((ts.hour - 4) / 24) * 2 * math.pi)
+ db.add(
+ SoilMoistureReading(
+ sensor_id=sensor.id,
+ reading_timestamp=ts,
+ moisture_percent=round(moisture, 1),
+ soil_temperature_c=round(temp, 1),
+ )
+ )
+ added += 1
+ db.commit()
+ print(f"✓ {len(sensors)} sensör × {HOURS}h = {added} taze okuma eklendi (son {HOURS}h, idempotent).")
+ finally:
+ db.close()
+
+
+if __name__ == "__main__":
+ seed_readings()
From eb85b264f6fab29a5b29c2ecbd5edfa428a7bacb Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:38:53 +0300
Subject: [PATCH 3/8] =?UTF-8?q?feat(dashboard):=20"Toprak=20Nemi=20Trendi"?=
=?UTF-8?q?=20ger=C3=A7ek=20toprak=20nemine=20ba=C4=9Fland=C4=B1=20(hava?=
=?UTF-8?q?=20nemi=20de=C4=9Fildi)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Grafik renderMoistureChart hava nemini (weather.humidity_percent) çiziyordu ama
başlık "Toprak Nemi Trendi" idi → yanıltıcı + "Sıcaklık & Nem" grafiğiyle tekrar.
- Backend: dashboard summary'ye soil_moisture_trend eklendi — son 24 saat saatlik
ortalama (DB-agnostik Python bucket, ~192 satır). DashboardMoisturePoint şeması.
- Frontend: dashboard grafiğe summary.soil_moisture_trend geçiriyor; renderMoistureChart
artık moisture_percent + hour (saatlik HH:MM) çiziyor, legend "Toprak Nemi %".
Doğrulandı: summary 24 saatlik trend döndürüyor (%43→%49); grafik gerçek toprak nemi
eğrisini çiziyor (00:00→23:00). 32 dashboard testi geçti, regresyon yok.
Co-Authored-By: Claude Opus 4.8
---
app/routers/dashboard.py | 24 ++++++++++++++++++++++++
app/schemas/dashboard.py | 8 ++++++++
frontend/src/lib/charts.js | 8 ++++----
frontend/src/lib/pages/dashboard.js | 2 +-
4 files changed, 37 insertions(+), 5 deletions(-)
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/frontend/src/lib/charts.js b/frontend/src/lib/charts.js
index 7d19aac..4edd052 100644
--- a/frontend/src/lib/charts.js
+++ b/frontend/src/lib/charts.js
@@ -51,12 +51,12 @@ export function rethemeCharts() {
}
export function renderMoistureChart(data) {
- const sorted = [...data].reverse();
- const labels = sorted.map(d => new Date(d.recorded_at).toLocaleDateString('tr'));
- const values = sorted.map(d => d.humidity_percent);
+ const points = Array.isArray(data) ? data : [];
+ const labels = points.map(d => new Date(d.hour).toLocaleTimeString('tr', { hour: '2-digit', minute: '2-digit' }));
+ const values = points.map(d => d.moisture_percent);
if (charts.moisture) charts.moisture.destroy();
charts.moisture = new Chart(document.getElementById('moistureChart'), {
- type: 'line', data: { labels, datasets: [{ label: 'Nem %', data: values, borderColor: '#3b82f6',
+ type: 'line', data: { labels, datasets: [{ label: 'Toprak Nemi %', data: values, borderColor: '#3b82f6',
backgroundColor: 'rgba(59,130,246,.1)', fill: true, tension: .4, pointRadius: 2 }] },
options: { responsive: true, plugins: { legend: { labels: { color: chartLegend() } } },
scales: { x: { ticks: { color: chartTick() }, grid: { color: chartGrid() } }, y: { ticks: { color: chartTick() }, grid: { color: chartGrid() } } } }
diff --git a/frontend/src/lib/pages/dashboard.js b/frontend/src/lib/pages/dashboard.js
index 8800a27..ac269fc 100644
--- a/frontend/src/lib/pages/dashboard.js
+++ b/frontend/src/lib/pages/dashboard.js
@@ -174,7 +174,7 @@ export async function loadDashboard() {
// Trend chart'lar — weather Bearer üzerinden gelir (api() artık _authHeaders kullanıyor).
const w = weather || [];
- renderMoistureChart(w);
+ renderMoistureChart((summary && summary.soil_moisture_trend) || []);
renderTempHumChart(w);
renderPrecipChart(w);
}
From fa22d1482e5463ab167127be2a425531b56a0b88 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:41:49 +0300
Subject: [PATCH 4/8] fix(frontend): defansif guard'lar + resolveAlert Bearer
auth
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- utils.js: showToast/updateStatus/updateClock null-guard'landı (eksik DOM
elemanında crash yerine sessiz no-op; updateClock saniyede bir koşuyor).
- router.js: navigate() bilinmeyen sayfa adında pageTitles[page]/page-${page}
null deref ile patlıyordu → erken guard + uyarı.
- charts.js: renderSensorStatsChart stats.moisture/soil_temperature yoksa
patlıyordu → guard (avg=0 artık geçerli sayılıyor).
- alerts.js: resolveAlert sabit 'X-API-Key: dev-api-key' kullanıyordu →
apiAuth (Bearer) ile değiştirildi (resolveFromBell ile tutarlı).
Co-Authored-By: Claude Opus 4.8
---
frontend/src/lib/charts.js | 2 +-
frontend/src/lib/pages/alerts.js | 18 ++++--------------
frontend/src/lib/router.js | 4 ++++
frontend/src/lib/utils.js | 8 +++++---
4 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/frontend/src/lib/charts.js b/frontend/src/lib/charts.js
index 4edd052..3dc8d60 100644
--- a/frontend/src/lib/charts.js
+++ b/frontend/src/lib/charts.js
@@ -246,7 +246,7 @@ export function renderDailyTrendChart(trends) {
}
export function renderSensorStatsChart(stats) {
- if (!stats || !stats.moisture.avg) return;
+ if (!stats || !stats.moisture || !stats.soil_temperature || stats.moisture.avg == null) return;
if (charts.sensorStats) charts.sensorStats.destroy();
charts.sensorStats = new Chart(document.getElementById('sensorStatsChart'), {
type: 'bar',
diff --git a/frontend/src/lib/pages/alerts.js b/frontend/src/lib/pages/alerts.js
index ee1a07a..27d0b1b 100644
--- a/frontend/src/lib/pages/alerts.js
+++ b/frontend/src/lib/pages/alerts.js
@@ -70,20 +70,10 @@ export async function loadAlerts() {
}
export async function resolveAlert(id) {
- try {
- const resp = await fetch(`${API_BASE}/api/alerts/${id}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json', 'X-API-Key': 'dev-api-key' },
- body: JSON.stringify({ is_resolved: true }),
- });
- if (resp.ok) {
- showToast('Uyarı çözüldü olarak işaretlendi', 'success');
- loadAlerts();
- } else {
- showToast(`Hata ${resp.status}`, 'error');
- }
- } catch (e) {
- showToast('Sunucuya ulaşılamadı', 'error');
+ const res = await apiAuth(`/api/alerts/${id}`, { method: 'PATCH', body: JSON.stringify({ is_resolved: true }) });
+ if (res) {
+ showToast('Uyarı çözüldü olarak işaretlendi', 'success');
+ loadAlerts();
}
}
diff --git a/frontend/src/lib/router.js b/frontend/src/lib/router.js
index bc0c1da..c08d9eb 100644
--- a/frontend/src/lib/router.js
+++ b/frontend/src/lib/router.js
@@ -27,6 +27,10 @@ export const pageTitles = {
* @param {Function} startHeroTipRotation - Hero tip rotation starter
*/
export function navigate(page, handlers, startHeroTipRotation) {
+ if (!document.getElementById(`page-${page}`) || !pageTitles[page]) {
+ console.warn(`navigate: bilinmeyen sayfa "${page}"`);
+ return;
+ }
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => {
n.classList.remove('active');
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
index 0897d80..6923a79 100644
--- a/frontend/src/lib/utils.js
+++ b/frontend/src/lib/utils.js
@@ -31,6 +31,7 @@ export function _escAttr(s) {
export function showToast(message, type = 'info', duration = 3500) {
const container = document.getElementById('toastContainer');
+ if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
@@ -56,10 +57,11 @@ export function showToast(message, type = 'info', duration = 3500) {
export function updateStatus(online) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
- dot.className = `status-dot ${online ? 'online' : 'offline'}`;
- text.textContent = online ? 'Sistem Aktif' : 'Bağlantı Yok';
+ if (dot) dot.className = `status-dot ${online ? 'online' : 'offline'}`;
+ if (text) text.textContent = online ? 'Sistem Aktif' : 'Bağlantı Yok';
}
export function updateClock() {
- document.getElementById('clockDisplay').textContent = new Date().toLocaleTimeString('tr');
+ const clockEl = document.getElementById('clockDisplay');
+ if (clockEl) clockEl.textContent = new Date().toLocaleTimeString('tr');
}
From 842fd86c351369af2e95c7fbfe9094517230b708 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:43:41 +0300
Subject: [PATCH 5/8] =?UTF-8?q?fix(monitoring):=20edge-case=20d=C3=BCzeltm?=
=?UTF-8?q?eleri=20(weather/irrigation/fertilizer/plant)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Weather sıcaklık aralığı: filter(Boolean) 0°C'yi düşürüyordu + tüm temp null
iken Math.min/max ±Infinity veriyordu → filter(!=null) + boş-guard ('—').
- approveIrrigation: tahmin yokken 0 L'lik program gönderebiliyordu → guard
(önce öneri hesapla uyarısı).
- Gübre NPK: undefined değer "undefined kg" basıyordu → '?? —'.
- fertilizerSchedule: null/boş yanıtta sessiz kalıyordu → empty-state + toast.
- analyzePlantImage: çift-submit guard (if btn.disabled return).
Co-Authored-By: Claude Opus 4.8
---
frontend/src/lib/pages/monitoring.js | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/frontend/src/lib/pages/monitoring.js b/frontend/src/lib/pages/monitoring.js
index 556de19..001ffa0 100644
--- a/frontend/src/lib/pages/monitoring.js
+++ b/frontend/src/lib/pages/monitoring.js
@@ -92,11 +92,11 @@ export async function loadWeather() {
const sorted = [...data].reverse();
// Cards
if (data.length) {
- const temps = data.map(d => d.temperature_c).filter(Boolean);
+ const temps = data.map(d => d.temperature_c).filter(t => t != null);
document.getElementById('weatherCards').innerHTML = `
🌡️
${data[0].temperature_c?.toFixed(1) || '—'}°C
Güncel Sıcaklık
💧
%${data[0].humidity_percent?.toFixed(1) || '—'}
Güncel Nem
- 📊
${Math.min(...temps).toFixed(1)}—${Math.max(...temps).toFixed(1)}°C
Sıcaklık Aralığı
+ 📊
${temps.length ? Math.min(...temps).toFixed(1) + '—' + Math.max(...temps).toFixed(1) : '—'}°C
Sıcaklık Aralığı
🌧️
${data.reduce((a,d) => a + (d.precipitation_mm||0), 0).toFixed(1)}mm
Toplam Yağış
`;
}
@@ -217,10 +217,11 @@ export async function predictIrrigation() {
export async function approveIrrigation() {
const sel = document.getElementById('irrApproveField');
if (!sel || !sel.value) { showToast('Lütfen bir tarla seç', 'warning'); return; }
+ if (_lastIrrigationRec == null) { showToast('Önce sulama önerisi hesapla', 'warning'); return; }
const body = {
field_id: parseInt(sel.value, 10),
scheduled_date: new Date().toISOString(),
- water_amount_liters: _lastIrrigationRec ?? 0,
+ water_amount_liters: _lastIrrigationRec,
};
const res = await apiAuth('/api/irrigation/schedules', { method: 'POST', body: JSON.stringify(body) });
if (res) {
@@ -272,9 +273,9 @@ export async function recommendFertilizer() {
🌱 ${r.crop_name_tr} — Gübreleme Önerisi
-
Azot (N)
${r.nitrogen_needed_kg} kg
-
Fosfor (P)
${r.phosphorus_needed_kg} kg
-
Potasyum (K)
${r.potassium_needed_kg} kg
+
Azot (N)
${r.nitrogen_needed_kg ?? '—'} kg
+
Fosfor (P)
${r.phosphorus_needed_kg ?? '—'} kg
+
Potasyum (K)
${r.potassium_needed_kg ?? '—'} kg
${r.recommendation}
`;
@@ -300,6 +301,9 @@ export async function fertilizerSchedule() {
${schedule.map(s => `| ${s.phase} | ${s.target_date} | ${s.fertilizer_type} | ${s.amount_kg_per_hectare} | ${s.notes} |
`).join('')}
`;
showToast('Gübreleme takvimi oluşturuldu', 'success');
+ } else {
+ document.getElementById('scheduleResult').innerHTML = 'Gübreleme takvimi oluşturulamadı.
';
+ showToast('Gübreleme takvimi alınamadı', 'error');
}
}
@@ -392,6 +396,7 @@ export async function analyzePlantImage() {
return;
}
const btn = document.getElementById('plantsAnalyzeBtn');
+ if (btn.disabled) return; // çift-submit guard
btn.disabled = true;
btn.textContent = '⏳ Analiz ediliyor...';
const fd = new FormData();
From 1a1581569171c5a0c60a0f0a9bf449b252dd6b89 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:50:29 +0300
Subject: [PATCH 6/8] =?UTF-8?q?fix(frontend):=20nice-to-have=20cilalar=20?=
=?UTF-8?q?=E2=80=94=20ki=C5=9Fisel=20selamlama=20+=20form=20temizleme=20+?=
=?UTF-8?q?=20escape=20sertle=C5=9Ftirme?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Dashboard hero selamı artık kullanıcı adıyla ("Hoş geldin, {ad}!") — admin'e
sabit "çiftçi" yerine rol-agnostik (index.html #heroGreetName + dashboard.js summary.user_name).
- fields.js: çiftlik/tarla/sensör başarılı eklendikten sonra form inputları
temizleniyor (tekrar açılınca eski değer kalmaz → yanlış tekrar-submit önlenir).
- Escape sertleştirme: alert mesajı + sensör seri no (kullanıcı-girdili) artık
_escAttr'dan geçiyor (XSS savunma derinliği; _escAttr bfa718d'de düzeltilmişti).
Co-Authored-By: Claude Opus 4.8
---
frontend/index.html | 2 +-
frontend/src/lib/pages/alerts.js | 2 +-
frontend/src/lib/pages/dashboard.js | 2 ++
frontend/src/lib/pages/fields.js | 5 +++--
frontend/src/lib/pages/monitoring.js | 2 +-
5 files changed, 8 insertions(+), 5 deletions(-)
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 @@ Dashboard
-
🌾 Hoş geldin, çiftçi!
+
🌾 Hoş geldin, çiftçi!
Çiftliğinden toplanan toprak, hava ve sulama bilgilerini tek bir ekranda gör. Akıllı asistan seninle çalışıyor.
🚜 — çiftlik
diff --git a/frontend/src/lib/pages/alerts.js b/frontend/src/lib/pages/alerts.js
index 27d0b1b..84c9ca5 100644
--- a/frontend/src/lib/pages/alerts.js
+++ b/frontend/src/lib/pages/alerts.js
@@ -60,7 +60,7 @@ export async function loadAlerts() {
${date} |
${severityLabel(a.severity)} |
${alertTypeLabel(a.alert_type)} |
-
${a.message} |
+
${_escAttr(a.message)} |
${status} |
`;
}
diff --git a/frontend/src/lib/pages/dashboard.js b/frontend/src/lib/pages/dashboard.js
index ac269fc..c44cac8 100644
--- a/frontend/src/lib/pages/dashboard.js
+++ b/frontend/src/lib/pages/dashboard.js
@@ -163,6 +163,8 @@ export async function loadDashboard() {
if (heroFarms) heroFarms.textContent = summary.farm_count;
if (heroSensors) heroSensors.textContent = summary.sensor_count;
if (heroReadings) heroReadings.textContent = (summary.soil_moisture_today || {}).reading_count || 0;
+ const heroGreet = document.getElementById('heroGreetName');
+ if (heroGreet) heroGreet.textContent = summary.user_name || 'çiftçi';
} else {
cards.innerHTML = `
diff --git a/frontend/src/lib/pages/fields.js b/frontend/src/lib/pages/fields.js
index d79b017..c037a03 100644
--- a/frontend/src/lib/pages/fields.js
+++ b/frontend/src/lib/pages/fields.js
@@ -142,7 +142,7 @@ export async function submitNewFarm() {
area_hectares: parseFloat(document.getElementById('nfArea').value) || null,
};
const res = await apiAuth('/api/farms/', { method: 'POST', body: JSON.stringify(body) });
- if (res) { showToast('Çiftlik eklendi ✅', 'success'); loadFields(); }
+ if (res) { ['nfName', 'nfCity', 'nfRegion', 'nfArea'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); showToast('Çiftlik eklendi ✅', 'success'); loadFields(); }
}
export async function submitNewField() {
@@ -156,7 +156,7 @@ export async function submitNewField() {
area_hectares: parseFloat(document.getElementById('ndArea').value) || null,
};
const res = await apiAuth('/api/fields', { method: 'POST', body: JSON.stringify(body) });
- if (res) { showToast('Tarla eklendi ✅', 'success'); loadFields(); }
+ if (res) { ['ndName', 'ndSoil', 'ndArea'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); showToast('Tarla eklendi ✅', 'success'); loadFields(); }
}
export async function editFarm(farmId, currentName) {
@@ -212,6 +212,7 @@ export async function submitNewSensor(fieldId) {
};
const res = await apiAuth('/api/sensors/', { method: 'POST', body: JSON.stringify(body) });
if (res) {
+ ['nsSerial', 'nsDepth'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
showToast('Sensör eklendi ✅', 'success');
if (currentFieldId) loadFieldDetail(currentFieldId);
}
diff --git a/frontend/src/lib/pages/monitoring.js b/frontend/src/lib/pages/monitoring.js
index 001ffa0..312a08f 100644
--- a/frontend/src/lib/pages/monitoring.js
+++ b/frontend/src/lib/pages/monitoring.js
@@ -60,7 +60,7 @@ export async function loadSensors(page = 1) {
document.getElementById('sensorsTable').innerHTML = sensors.map(s => `
- | ${s.id} | ${s.sensor_type} | ${s.serial_number} |
+ ${s.id} | ${s.sensor_type} | ${_escAttr(s.serial_number)} |
${s.status} |
`).join('');
From 118901947c220cd55dcd95d95db92c92cb9b8645 Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Mon, 8 Jun 2026 23:57:19 +0300
Subject: [PATCH 7/8] chore(demo): make demo-readings target + demo_script notu
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
seed_demo_readings'i Makefile'a bağla (PYTHONPATH ile) + demo_script.md'de
"DB sıfırlamadan okuma tazeleme" alternatifi olarak belgele. Demo öncesi
re-seed yerine bunu da koşabilir (mevcut veriyi koruyarak).
Co-Authored-By: Claude Opus 4.8
---
Makefile | 5 ++++-
docs/demo_script.md | 2 ++
2 files changed, 6 insertions(+), 1 deletion(-)
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/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ı:**
From d7485aa6a3b513409ac39ed70b5b734eda0aa2ec Mon Sep 17 00:00:00 2001
From: Ohualtex <230626673+Ohualtex@users.noreply.github.com>
Date: Tue, 9 Jun 2026 00:13:49 +0300
Subject: [PATCH 8/8] =?UTF-8?q?fix(frontend):=20pre-PR=20review=20d=C3=BCz?=
=?UTF-8?q?eltmeleri=20=E2=80=94=20render.js=20i18n=20(eksikti)=20+=20cila?=
=?UTF-8?q?lar?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adversarial pre-PR review (22 bulgu → 9 doğrulandı) bir gerçek must-fix yakaladı:
- render.js (Tarla Detayı + Bitki Sağlığı sonuç renderer'ı, demo merkezi) labels.js'i
HİÇ almamıştı → enum'lar hâlâ İngilizce (pending/leaf_spot/critical) iken yan
sayfalar Türkçe'ydi (yarı-çevrilmiş UI). irrigation/diagnosis/severity Türkçe'ye
çevrildi (ham değer yalnız CSS class'ında). Doğrulandı: hamİngilizce=false.
Düşük-öncelik cilalar (review'dan):
- apiAuth: network hatasında (TypeError) toast → resolveAlert/Bell "Çöz" feedback'i
(HTTP hatalarında çift-toast yok; apiAuth zaten onları gösteriyor).
- alerts.js: kullanılmayan API_BASE import'u kaldırıldı.
- dashboard: hero selamı onboarding (0-çiftlik) branch'inde de çalışsın (hoist).
- monitoring: 2 toast'ta ham 'pending'/status → irrigationStatusLabel.
- charts: trend etiketine gün ipucu (24h sınırında HH:00 tekrarı çözüldü).
Co-Authored-By: Claude Opus 4.8
---
frontend/src/lib/api.js | 1 +
frontend/src/lib/charts.js | 2 +-
frontend/src/lib/pages/alerts.js | 2 +-
frontend/src/lib/pages/dashboard.js | 6 ++++--
frontend/src/lib/pages/monitoring.js | 4 ++--
frontend/src/lib/render.js | 11 ++++++-----
6 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js
index 31d1689..31b72c9 100644
--- a/frontend/src/lib/api.js
+++ b/frontend/src/lib/api.js
@@ -104,6 +104,7 @@ export async function apiAuth(endpoint, options = {}) {
return await res.json();
} catch (e) {
console.warn(`API Auth Error: ${endpoint}`, e);
+ if (e instanceof TypeError) _showToast?.('Sunucuya ulaşılamadı', 'error');
return null;
}
}
diff --git a/frontend/src/lib/charts.js b/frontend/src/lib/charts.js
index 3dc8d60..475a0d5 100644
--- a/frontend/src/lib/charts.js
+++ b/frontend/src/lib/charts.js
@@ -52,7 +52,7 @@ export function rethemeCharts() {
export function renderMoistureChart(data) {
const points = Array.isArray(data) ? data : [];
- const labels = points.map(d => new Date(d.hour).toLocaleTimeString('tr', { hour: '2-digit', minute: '2-digit' }));
+ const labels = points.map(d => new Date(d.hour).toLocaleString('tr', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }));
const values = points.map(d => d.moisture_percent);
if (charts.moisture) charts.moisture.destroy();
charts.moisture = new Chart(document.getElementById('moistureChart'), {
diff --git a/frontend/src/lib/pages/alerts.js b/frontend/src/lib/pages/alerts.js
index 84c9ca5..713b104 100644
--- a/frontend/src/lib/pages/alerts.js
+++ b/frontend/src/lib/pages/alerts.js
@@ -7,7 +7,7 @@
============================================================ */
import { _escAttr, showToast } from "../utils.js";
-import { api, apiAuth, API_BASE } from "../api.js";
+import { api, apiAuth } from "../api.js";
import { _skeletonCards, _skeletonBlock, _setBusy } from "../skeleton.js";
import { severityLabel, alertTypeLabel } from "../labels.js";
diff --git a/frontend/src/lib/pages/dashboard.js b/frontend/src/lib/pages/dashboard.js
index c44cac8..2aad7f3 100644
--- a/frontend/src/lib/pages/dashboard.js
+++ b/frontend/src/lib/pages/dashboard.js
@@ -147,6 +147,10 @@ export async function loadDashboard() {
]);
setApiOnline(summary !== null);
updateStatus(getApiOnline());
+ if (summary) {
+ const heroGreet = document.getElementById('heroGreetName');
+ if (heroGreet) heroGreet.textContent = summary.user_name || 'çiftçi';
+ }
if (summary && summary.scope === 'user' && summary.farm_count === 0) {
// Boş hesap — onboarding banner (REBUILD Faz 6)
@@ -163,8 +167,6 @@ export async function loadDashboard() {
if (heroFarms) heroFarms.textContent = summary.farm_count;
if (heroSensors) heroSensors.textContent = summary.sensor_count;
if (heroReadings) heroReadings.textContent = (summary.soil_moisture_today || {}).reading_count || 0;
- const heroGreet = document.getElementById('heroGreetName');
- if (heroGreet) heroGreet.textContent = summary.user_name || 'çiftçi';
} else {
cards.innerHTML = `
diff --git a/frontend/src/lib/pages/monitoring.js b/frontend/src/lib/pages/monitoring.js
index 312a08f..859ade0 100644
--- a/frontend/src/lib/pages/monitoring.js
+++ b/frontend/src/lib/pages/monitoring.js
@@ -225,7 +225,7 @@ export async function approveIrrigation() {
};
const res = await apiAuth('/api/irrigation/schedules', { method: 'POST', body: JSON.stringify(body) });
if (res) {
- showToast('Sulama programa eklendi ✅ (durum: pending)', 'success');
+ showToast(`Sulama programa eklendi ✅ (durum: ${irrigationStatusLabel('pending')})`, 'success');
irrigationTotal = 0; // sayacı tazele
loadIrrigation(1);
}
@@ -237,7 +237,7 @@ export async function updateIrrigationStatus(scheduleId, status) {
method: 'PATCH', body: JSON.stringify({ status }),
});
if (res) {
- showToast(`Sulama durumu: ${status}`, 'success');
+ showToast(`Sulama durumu: ${irrigationStatusLabel(status)}`, 'success');
const currentFieldId = getCurrentFieldId();
if (currentFieldId) loadFieldDetail(currentFieldId);
}
diff --git a/frontend/src/lib/render.js b/frontend/src/lib/render.js
index 9672126..e2c2677 100644
--- a/frontend/src/lib/render.js
+++ b/frontend/src/lib/render.js
@@ -8,6 +8,7 @@
*/
import { _escAttr, _fmtDate, _fmtNumber, _STATUS_EMOJI, _STATUS_LABEL } from "./utils.js";
+import { irrigationStatusLabel, diagnosisLabel, severityLabel } from "./labels.js";
export function renderFieldDetail(d) {
const cropName = d.crop ? d.crop.name : 'Ekili bitki yok';
@@ -34,13 +35,13 @@ export function renderFieldDetail(d) {
? `
`
: '—';
- return `
| ${_fmtDate(i.scheduled_date)} | ${amt} | ${i.duration_min ?? '—'} dk | ${_escAttr(i.status)} | ${actions} |
`;
+ return `
| ${_fmtDate(i.scheduled_date)} | ${amt} | ${i.duration_min ?? '—'} dk | ${_escAttr(irrigationStatusLabel(i.status))} | ${actions} |
`;
}).join('') || '
| Sulama kaydı yok. |
';
// Hastalık geçmişi
const disRows = (d.disease_history || []).map(h => {
const conf = h.confidence_score != null ? `%${_fmtNumber(h.confidence_score * 100, 0)}` : '—';
- return `
| ${_fmtDate(h.captured_at)} | ${_escAttr(h.diagnosis || '—')} | ${conf} | ${_escAttr(h.severity || '—')} |
`;
+ return `
| ${_fmtDate(h.captured_at)} | ${_escAttr(diagnosisLabel(h.diagnosis))} | ${conf} | ${_escAttr(severityLabel(h.severity))} |
`;
}).join('') || '
| Hastalık analizi yok. |
';
// Toprak analizi (en yeni)
@@ -53,7 +54,7 @@ export function renderFieldDetail(d) {
// Açık uyarılar
const alertRows = (d.open_alerts || []).map(a =>
- `
${_escAttr(a.severity)} · ${_escAttr(a.message)} ${_fmtDate(a.created_at)}
`
+ `
${_escAttr(severityLabel(a.severity))} · ${_escAttr(a.message)} ${_fmtDate(a.created_at)}
`
).join('') || '
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 = `