Frontend Kritik Notlar
NEXT_PUBLIC_* Tuzağı
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/Dockerfile—ARG NEXT_PUBLIC_WS_URLveENV NEXT_PUBLIC_WS_URLsatırları silindi.docker-compose.yml— frontend service'inbuild.args+environmentblokları 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ı:
- Browser dışı (SSR/build) →
ws://localhost:8000/wsgüvenli fallback (gerçek WS bağlantısı SSR'da kurulmaz) - Localhost dev →
ws://localhost:8000/ws(backend portuna doğrudan bağlan) - Env override (
NEXT_PUBLIC_WS_URL) → YALNIZCA sayfa protokolüyle uyumluysa kabul edilir - 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)
ws:// kullanılamazTarayı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:
getWsUrl()üretim aşaması (yukarıda) — protokol uyumsuz env değeri reddedilir, host-relativewss://üretilir.WebSocketClient.connect()çağrı aşaması — son bir guard daha; ayrıca constructortry/catchile 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ı:
localStorageerişimitry/catchile sarılı — SSR/edge senaryolarındalocalStoragethrow edebilir.new WebSocketClient()+connect()kombinasyonutry/catchiçinde — herhangi bir sync hata yakalanırsasetClient(null)+setState("error"). Subscribe() => {}no-op döner; WS'e bağımlı componentleruseWebSocketOptional()ile null kontrolü yaparak graceful degrade çalışır.WebSocketErrorBoundaryprovider'ı sarıyor — yukarıdaki iki katman tutmazsa boundary class componentcomponentDidCatchile 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ış:
getDerivedStateFromError→hasError=true. Render'daprops.fallbackverilmişse onu, yoksanulldö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.ts | User, Region, Subregion, Gateway, Device, Measurement, Alarm, PaginatedResponse |
types/alarm.ts | 57 alarm tipi tanımı, NotificationChannels, AlarmPolicy, AlarmIncident, AlarmEvent |
types/widget.ts | 11 widget tipi, 40+ realtime parametre, WidgetConfig |
types/zigbee.ts | ZigbeeGateway, ZigbeeDevice, ZigbeeEnergyMeasurement, DeviceControlCommand |
types/contact.ts | Email, 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:
device.template_id(kolonun kendisi)device.template?.id(eager-loaded relationship)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.