Load tool views on demand with isolated retry states

This commit is contained in:
2026-09-06 10:41:50 +03:00
parent 1a62a876e6
commit fef6f2ab89
4 changed files with 63 additions and 39 deletions
File diff suppressed because one or more lines are too long
+8
View File
@@ -631,3 +631,11 @@ Sınırlar: Bir parçanın işlenmesi tüm şartlarının anlam bakımından tas
Bozuk JSON'un boş gövdeye çevrilip varsayılan anahtar oluşturması kaldırıldı. Anahtar adı metin ve en fazla 120 karakter olmalı; çevresindeki boşluklar temizlenir. GET/POST/DELETE dışındaki metotlar 405 döner. Bozuk JSON'un boş gövdeye çevrilip varsayılan anahtar oluşturması kaldırıldı. Anahtar adı metin ve en fazla 120 karakter olmalı; çevresindeki boşluklar temizlenir. GET/POST/DELETE dışındaki metotlar 405 döner.
Doğrulama: yerel izole HTTP testinde eşzamanlı 16 oluşturma isteğinden tam 10 başarı, 6 kota reddi; bir anahtar iptal edilince yeni kayıt mümkün. Bozuk JSON/geçersiz ad kayıt oluşturmuyor. Gerçek SQLite testinde kota reddi sonrası işlem kapanıyor, iptal ve yeni kayıt çalışıyor. Tüm 137 test geçti; ön yüz değişmediğinden derleme tekrarlanmadı. Farklı sunucu süreçlerinden yük testi yapılmadı. Planın diğer kabul işleri ve canlı yayın açık. Doğrulama: yerel izole HTTP testinde eşzamanlı 16 oluşturma isteğinden tam 10 başarı, 6 kota reddi; bir anahtar iptal edilince yeni kayıt mümkün. Bozuk JSON/geçersiz ad kayıt oluşturmuyor. Gerçek SQLite testinde kota reddi sonrası işlem kapanıyor, iptal ve yeni kayıt çalışıyor. Tüm 137 test geçti; ön yüz değişmediğinden derleme tekrarlanmadı. Farklı sunucu süreçlerinden yük testi yapılmadı. Planın diğer kabul işleri ve canlı yayın açık.
### D09 — Araç ekranlarını ihtiyaç anında yükleme
19 araç görünümü başlangıçtaki statik importlardan çıkarıldı; kullanıcı ilgili menüyü açınca dinamik yüklenir. Yükleme durumu erişilebilir status olarak gösterilir. Görünüm yüklenemezse hata sınırı yalnız o görünümü kapsar; yeniden deneme ana uygulamayı veya kayıt durumunu yeniden başlatmaz. Yönetici ekranının mevcut yetki koşulu korunur.
Üretim derlemesi ölçümü: ana JavaScript 1.097,39 KB'den 526,09 KB'ye (gzip 306,24 KB'den 156,35 KB'ye) düştü. Grafik ortak paketi 384,20 KB olarak ayrıldı. Bunlar dosya boyutlarıdır; gerçek ağda ilk açılış süresi/LCP ölçülmüş değildir. 940 KB ExcelJS ve 500 KB üstü ana paket uyarıları hâlâ var; performans maddesi tamamen kapanmadı.
Yerel tarayıcı kabulünde KDV, API, Belge Kasası ve Analitik ekranları açıldı; ana ihale listesine dönüşte beş sentetik ihale korundu. Kanıt qa-deferred-views.json. Geliştirme sunucusunda menü geçişi kabulüdür; üretim paketi ağ hatası/yeniden deneme ve tüm 19 ekranın etkileşim kabulü değildir. Tüm 137 test ve üretim derlemesi geçti. Canlı yayın yapılmadı.
+26 -39
View File
@@ -8,36 +8,41 @@ import { SummaryCards } from './components/SummaryCards';
import { LeftSidebar } from './components/LeftSidebar'; import { LeftSidebar } from './components/LeftSidebar';
import { TenderList } from './components/TenderList'; import { TenderList } from './components/TenderList';
import { RightSidebar } from './components/RightSidebar'; import { RightSidebar } from './components/RightSidebar';
import { SmartFilterBuilder } from './components/SmartFilterBuilder';
import { DetailedSearchModal } from './components/DetailedSearchModal'; import { DetailedSearchModal } from './components/DetailedSearchModal';
import { TenderDetailModal } from './components/TenderDetailModal'; import { TenderDetailModal } from './components/TenderDetailModal';
import { CompanyScannerModal } from './components/CompanyScannerModal'; import { CompanyScannerModal } from './components/CompanyScannerModal';
import { AiSpecAnalyzerModal } from './components/AiSpecAnalyzerModal'; import { AiSpecAnalyzerModal } from './components/AiSpecAnalyzerModal';
import { UrunAramaModal } from './components/UrunAramaModal'; import { UrunAramaModal } from './components/UrunAramaModal';
import { KanbanBoard } from './components/KanbanBoard';
import { DailyBulletin } from './components/tools/DailyBulletin';
import { ResultSearch } from './components/tools/ResultSearch';
import { TenderCalendar } from './components/tools/TenderCalendar';
import { ContractSearch } from './components/tools/ContractSearch';
import { VatCalculator } from './components/tools/VatCalculator';
import { SinirDeger } from './components/tools/SinirDeger';
import { KullanimKilavuzu } from './components/KullanimKilavuzu'; import { KullanimKilavuzu } from './components/KullanimKilavuzu';
import { MobilFiltreCekmecesi } from './components/MobilFiltreCekmecesi'; import { MobilFiltreCekmecesi } from './components/MobilFiltreCekmecesi';
import { PriceAnalysis } from './components/tools/PriceAnalysis';
import { CompetitorRadar } from './components/tools/CompetitorRadar';
import { DocumentVault } from './components/tools/DocumentVault';
import { AnalyticsDashboard } from './components/tools/AnalyticsDashboard';
import { RedFlags } from './components/tools/RedFlags';
import { TedTenders } from './components/tools/TedTenders';
import { Pricing } from './components/tools/Pricing';
import { KikDecisions } from './components/tools/KikDecisions';
import { ApiWebhooks } from './components/tools/ApiWebhooks';
import { PwaSettings } from './components/tools/PwaSettings';
import { AiModelSettings } from './components/tools/AiModelSettings';
import { LoginPage } from './components/LoginPage'; import { LoginPage } from './components/LoginPage';
import { LandingPage } from './components/LandingPage'; import { LandingPage } from './components/LandingPage';
import { DeferredView } from './components/DeferredView';
const toolViews = {
smart_filter_builder: () => import('./components/SmartFilterBuilder').then(module => ({ default: module.SmartFilterBuilder })),
bulten: () => import('./components/tools/DailyBulletin').then(module => ({ default: module.DailyBulletin })),
sonuc: () => import('./components/tools/ResultSearch').then(module => ({ default: module.ResultSearch })),
takvim: () => import('./components/tools/TenderCalendar').then(module => ({ default: module.TenderCalendar })),
sozlesme: () => import('./components/tools/ContractSearch').then(module => ({ default: module.ContractSearch })),
kdv: () => import('./components/tools/VatCalculator').then(module => ({ default: module.VatCalculator })),
sinir: () => import('./components/tools/SinirDeger').then(module => ({ default: module.SinirDeger })),
fiyat: () => import('./components/tools/PriceAnalysis').then(module => ({ default: module.PriceAnalysis })),
kanban: () => import('./components/KanbanBoard').then(module => ({ default: module.KanbanBoard })),
rakip_radar: () => import('./components/tools/CompetitorRadar').then(module => ({ default: module.CompetitorRadar })),
dokuman_deposu: () => import('./components/tools/DocumentVault').then(module => ({ default: module.DocumentVault })),
analitik_dashboard: () => import('./components/tools/AnalyticsDashboard').then(module => ({ default: module.AnalyticsDashboard })),
kirmizi_bayrak: () => import('./components/tools/RedFlags').then(module => ({ default: module.RedFlags })),
ted_eu: () => import('./components/tools/TedTenders').then(module => ({ default: module.TedTenders })),
kik_kararlari: () => import('./components/tools/KikDecisions').then(module => ({ default: module.KikDecisions })),
planlar: () => import('./components/tools/Pricing').then(module => ({ default: module.Pricing })),
api_webhooks: () => import('./components/tools/ApiWebhooks').then(module => ({ default: module.ApiWebhooks })),
pwa_settings: () => import('./components/tools/PwaSettings').then(module => ({ default: module.PwaSettings })),
ai_ayarlari: () => import('./components/tools/AiModelSettings').then(module => ({ default: module.AiModelSettings })),
};
const MainContent = () => { const MainContent = () => {
const { activeTab, isAdmin, saveError, savingCount } = useApp(); const { activeTab, isAdmin, saveError, savingCount } = useApp();
@@ -62,26 +67,8 @@ const MainContent = () => {
</> </>
)} )}
{/* Dedicated Tool Views */} {toolViews[activeTab] && (activeTab !== 'ai_ayarlari' || isAdmin) &&
{activeTab === 'smart_filter_builder' && <SmartFilterBuilder />} <DeferredView key={activeTab} load={toolViews[activeTab]} />}
{activeTab === 'bulten' && <DailyBulletin />}
{activeTab === 'sonuc' && <ResultSearch />}
{activeTab === 'takvim' && <TenderCalendar />}
{activeTab === 'sozlesme' && <ContractSearch />}
{activeTab === 'kdv' && <VatCalculator />}
{activeTab === 'sinir' && <SinirDeger />}
{activeTab === 'fiyat' && <PriceAnalysis />}
{activeTab === 'kanban' && <KanbanBoard />}
{activeTab === 'rakip_radar' && <CompetitorRadar />}
{activeTab === 'dokuman_deposu' && <DocumentVault />}
{activeTab === 'analitik_dashboard' && <AnalyticsDashboard />}
{activeTab === 'kirmizi_bayrak' && <RedFlags />}
{activeTab === 'ted_eu' && <TedTenders />}
{activeTab === 'kik_kararlari' && <KikDecisions />}
{activeTab === 'planlar' && <Pricing />}
{activeTab === 'api_webhooks' && <ApiWebhooks />}
{activeTab === 'pwa_settings' && <PwaSettings />}
{activeTab === 'ai_ayarlari' && isAdmin && <AiModelSettings />}
{/* Global Modals */} {/* Global Modals */}
<DetailedSearchModal /> <DetailedSearchModal />
+22
View File
@@ -0,0 +1,22 @@
import React, { Suspense, lazy, useMemo, useState } from 'react';
class ViewErrorBoundary extends React.Component {
state = { failed: false };
static getDerivedStateFromError() { return { failed: true }; }
render() {
if (this.state.failed) return <div role="alert" className="p-4 border border-red-200 rounded-xl bg-red-50 text-red-900">
<p>Bu ekran yüklenemedi. Bağlantınızı kontrol edip yeniden deneyebilirsiniz.</p>
<button type="button" onClick={this.props.retry} className="mt-3 px-4 py-2 rounded-lg border border-red-300">Yeniden dene</button>
</div>;
return this.props.children;
}
}
/** Retry only this view; keep the application and saved work mounted. */
export function DeferredView({ load }) {
const [attempt, setAttempt] = useState(0);
const View = useMemo(() => lazy(load), [load, attempt]);
return <ViewErrorBoundary key={attempt} retry={() => setAttempt(n => n + 1)}>
<Suspense fallback={<p role="status" className="p-4 text-slate-600">Ekran yükleniyor…</p>}><View /></Suspense>
</ViewErrorBoundary>;
}