Ana içeriğe geç

Frontend Kritik Notlar

NEXT_PUBLIC_* Tuzağı

DİKKAT

process.env.NEXT_PUBLIC_* değişkenleri build time'da literal string olarak bake edilir. Runtime'da docker-compose environment: ile değiştirilemez!

Yanlış Kullanım

// ❌ ASLA böyle kullanma!
const apiUrl = process.env.NEXT_PUBLIC_API_URL
fetch(`${apiUrl}/api/devices`)

Doğru Kullanım

// ✅ Her zaman siteConfig kullan
import { siteConfig } from '@/config/site'
fetch(`${siteConfig.apiUrl}/devices`)
// WebSocket için de aynı pattern:
const ws = new WebSocket(`${siteConfig.wsUrl}/telemetry?token=...`)

NEXT_PUBLIC_WS_URL — artık BAKE EDİLMİYOR (PR #276)

PR #276'dan itibaren NEXT_PUBLIC_WS_URL Docker image'ına gömülmüyor:

  • frontend/DockerfileARG NEXT_PUBLIC_WS_URL ve ENV NEXT_PUBLIC_WS_URL satırları silindi.
  • docker-compose.yml — frontend service'in build.args + environment blokları temizlendi.
  • .github/workflows/{ci,deploy,release}.yml — build-arg + env injection kaldırıldı.

Eğer geriye dönük olarak biri NEXT_PUBLIC_WS_URL set etmeye devam ederse getWsUrl() runtime guard'ı protokol uyumsuzsa env değerini sessizce reddedip host-relative fallback kullanır + console.warn üretir.

site.ts Nasıl Çalışır

siteConfig iki getter (apiUrl, wsUrl) üzerinden çalışır; her erişimde tarayıcı/SSR bağlamına göre URL yeniden hesaplanır. Bu sayede build-time bake değerleri runtime'da etkisiz hale gelir.

apiUrl

function getApiUrl(): string {
// Server-side (SSR) — dogrudan backend'e eris
if (typeof window === 'undefined') {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
return `${backendUrl}/api`
}
// Client-side (browser) — same-origin, Next.js rewrites proxy eder
return '/api'
}

getWsUrl — host-relative öncelikli (PR #276)

getWsUrl() PR #276 ile yeniden yazıldı. Yeni öncelik sırası:

  1. Browser dışı (SSR/build)ws://localhost:8000/ws güvenli fallback (gerçek WS bağlantısı SSR'da kurulmaz)
  2. Localhost devws://localhost:8000/ws (backend portuna doğrudan bağlan)
  3. Env override (NEXT_PUBLIC_WS_URL)YALNIZCA sayfa protokolüyle uyumluysa kabul edilir
  4. Production fallback → host-relative ${ws|wss}://${window.location.host}/ws (nginx /ws → backend:8000 proxy)
function getWsUrl(): string {
if (typeof window === 'undefined') return 'ws://localhost:8000/ws'

const { protocol, hostname, host } = window.location
const expectedScheme = protocol === 'https:' ? 'wss:' : 'ws:'

if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'ws://localhost:8000/ws'
}

// Env override — sadece protokol uyumluysa kabul (defansif)
const envUrl = process.env.NEXT_PUBLIC_WS_URL
if (envUrl) {
try {
const parsed = new URL(envUrl)
if (parsed.protocol === expectedScheme) return envUrl
console.warn(
`[siteConfig] NEXT_PUBLIC_WS_URL protokol uyumsuz `
+ `(${parsed.protocol} vs ${expectedScheme}), host-relative fallback`,
)
} catch {
console.warn('[siteConfig] NEXT_PUBLIC_WS_URL geçersiz URL formatı')
}
}

// Production: same-origin host-relative — nginx /ws → backend:8000 proxy
return `${expectedScheme}//${host}/ws`
}

Tam dosya: frontend/src/config/site.ts:42-76.

Next.js Rewrites (CORS Bypass)

// next.config.js
async rewrites() {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
return [{
source: '/api/:path*',
destination: `${backendUrl}/api/:path*`
}]
}

Bu sayede browser'dan gelen /api/* istekleri aynı domain'den geldiği için CORS sorunu olmaz.


Mixed-Content Guard (PR #276)

HTTPS sayfada ws:// kullanılamaz

Tarayıcı new WebSocket("ws://...") çağrısını HTTPS sayfada constructor seviyesinde sync SecurityError ile reddeder. Önceden bu hata WebSocketProvider Providers ağacının tepesinde yakalanmıyordu — tüm React tree unmount olup "Application error: a client-side exception has occurred" gösteriyordu.

Korumalar iki katmanlı uygulanıyor:

  1. getWsUrl() üretim aşaması (yukarıda) — protokol uyumsuz env değeri reddedilir, host-relative wss:// üretilir.
  2. WebSocketClient.connect() çağrı aşaması — son bir guard daha; ayrıca constructor try/catch ile sarılır:
// frontend/src/lib/websocket/client.ts:59-81
if (
typeof window !== "undefined" &&
window.location.protocol === "https:" &&
wsUrl.startsWith("ws://")
) {
console.error("[WebSocketClient] Mixed-content: HTTPS sayfada ws:// reddedildi")
this.setState("error")
return
}

try {
this.ws = new WebSocket(wsUrl)
} catch (err) {
// SecurityError, SyntaxError vb. — constructor sync fırlatabilir
console.error("[WebSocketClient] WebSocket constructor exception:", err)
this.setState("error")
return
}

WebSocketState tipine "error" value'su eklendi (frontend/src/lib/websocket/types.ts:61-67); reconnect denenmez (URL/protokol fatal).

Sorun yaşıyorsan

Aynı semptomu görüyorsan: Frontend Application Error troubleshooting.


WebSocketProvider Defensive Pattern (PR #276)

WebSocketProvider (frontend/src/lib/websocket/context.tsx) Providers ağacının tepesinde olduğu için herhangi bir senkron exception sayfayı tamamen çökertirdi. PR #276 sonrası provider üç katmanlı korumalı:

  1. localStorage erişimi try/catch ile sarılı — SSR/edge senaryolarında localStorage throw edebilir.
  2. new WebSocketClient() + connect() kombinasyonu try/catch içinde — herhangi bir sync hata yakalanırsa setClient(null) + setState("error"). Subscribe () => {} no-op döner; WS'e bağımlı componentler useWebSocketOptional() ile null kontrolü yaparak graceful degrade çalışır.
  3. WebSocketErrorBoundary provider'ı sarıyor — yukarıdaki iki katman tutmazsa boundary class component componentDidCatch ile yakalar; fallback <>{children}</> (provider'sız children render).
// frontend/src/components/providers.tsx:28-44
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<WebSocketErrorBoundary fallback={<>{children}</>}>
<WebSocketProvider>
<AlarmListener />
{children}
</WebSocketProvider>
</WebSocketErrorBoundary>
<Toaster ... />
</ThemeProvider>
</QueryClientProvider>

WebSocketErrorBoundary

Yeni class component (frontend/src/lib/websocket/WebSocketErrorBoundary.tsx). Amacı:

  • Tek görev: WS katmanında fırlayan herhangi bir senkron hatayı yakalamak.
  • Davranış: getDerivedStateFromErrorhasError=true. Render'da props.fallback verilmişse onu, yoksa null döner.
  • Tipik kullanım: <WebSocketErrorBoundary fallback={<>{children}</>}> — WS çökerse de sayfa açılmaya devam eder, sadece "live data" göstergesi düşer.

Bu pattern WS'e bağımlı UI componentlerinin useWebSocketOptional() (null güvenli) hook'unu kullanmasını gerektirir.


TypeScript Tip Tanımları

Dosyaİçerik
types/index.tsUser, Region, Subregion, Gateway, Device, Measurement, Alarm, PaginatedResponse
types/alarm.ts57 alarm tipi tanımı, NotificationChannels, AlarmPolicy, AlarmIncident, AlarmEvent
types/widget.ts11 widget tipi, 40+ realtime parametre, WidgetConfig
types/zigbee.tsZigbeeGateway, ZigbeeDevice, ZigbeeEnergyMeasurement, DeviceControlCommand
types/contact.tsEmail, SMS, WhatsApp contact'ları

i18n Yapılandırması

  • Desteklenen diller: Türkçe (TR), İngilizce (EN)
  • Varsayılan: Türkçe
  • Algılama: localStorage → navigator language
  • Cache: localStorage
  • Kullanım: const { t } = useTranslation(); t('key')

Device Edit Form — Template Initialization

Cihaz düzenleme form'u açılırken template_id üç farklı kaynaktan gelebilir. Bu kaynakların drift'i form reset bug'ına yol açar.

Sorun

Backend response'unda template_id üç noktada yer alabilir:

  1. device.template_id (kolonun kendisi)
  2. device.template?.id (eager-loaded relationship)
  3. device.extra_metadata.templateId (eski legacy alan)

Bu kaynaklar arasında drift varsa (ör. template_id=null ama extra_metadata.templateId="sofar-hyd"), form initial state belirsizdir ve dropdown reset olur.

Çözüm: useFormDefaults() Hook

function useFormDefaults(device: Device) {
return useMemo(() => ({
template_id: resolveTemplateId(device),
// ... diğer alanlar
}), [device])
}

function resolveTemplateId(device: Device): string | null {
// Öncelik zinciri: kolon → relationship → metadata
if (device.template_id) return device.template_id
if (device.template?.id) return device.template.id
if (device.extra_metadata?.templateId) return device.extra_metadata.templateId
return null
}

Bu hook React Hook Form'un defaultValues'una verilir; form mount'ta deterministik şekilde initialize olur.

extra_metadata Preservation

Partial update sırasında frontend'in tüm metadata'yı kaybetmesini engellemek için ham extra_metadata objesi spread ile korunur:

const onSubmit = (values: FormValues) => {
updateDevice({
...values,
extra_metadata: {
...device.extra_metadata, // mevcut metadata korunur
templateId: values.template_id, // sadece güncellenen alan
},
})
}

Backend tarafında shallow merge yapılır (bkz. API Endpoint Listesi). Ancak frontend'in de eski değerleri request body'de tutması, network failure / retry senaryolarında veri tutarlılığını garanti eder.