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() {
${_escAttr(a.message)}
- ${_escAttr(a.severity)} + ${_escAttr(severityLabel(a.severity))}
`).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 = `
-

🧪 Sonuç: ${data.diagnosis}

+

🧪 Sonuç: ${_escAttr(diagnosisLabel(data.diagnosis))}

Güven: %${conf}

-

Şiddet: ${sev}

+

Şiddet: ${_escAttr(severityLabel(sev))}

Model: ${data.model_version}

Tüm sınıf skorları
    `;