feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar

Roadmap'in 6 maddesi de gerçek çalışır halde:

1) Geçmiş veri motoru (EKAP v2)
   - server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
     ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
     yaklaşık maliyet, teklif sayısı), TR firma adı normalize
   - server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
   - server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
   - server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
   - /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
     rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor

2) Gerçek AI şartname analizi
   - server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
     Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
   - pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
   - AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata

3) KİK kurul kararları
   - server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
     düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
   - KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri

4) Fiyatlandırma + ödeme
   - server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
   - Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı

5) TED (AB ihaleleri)
   - server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
   - rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü

6) Kırmızı bayrak & şeffaflık
   - analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
     aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
   - RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
This commit is contained in:
2026-08-28 10:37:50 +03:00
parent 3e129c2988
commit 168052c267
26 changed files with 3121 additions and 219 deletions
+110
View File
@@ -43,3 +43,113 @@
.pulse-glow {
animation: pulseGlow 2s infinite ease-in-out;
}
/* ============ Landing page (lp-) ============ */
.lp-root { color-scheme: dark; }
.lp-grid {
background-image:
linear-gradient(rgba(148, 163, 184, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.05) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: radial-gradient(ellipse 90% 60% at 50% 0%, #000 55%, transparent 100%);
}
@keyframes lpBlob {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(50px, -40px) scale(1.12); }
66% { transform: translate(-35px, 30px) scale(0.94); }
}
.lp-blob { animation: lpBlob 18s ease-in-out infinite; }
@keyframes lpHeroIn {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
.lp-hero-in { opacity: 0; animation: lpHeroIn 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) forwards; }
@keyframes lpGradientMove {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.lp-gradient-text {
background: linear-gradient(90deg, #22d3ee, #a78bfa, #fbbf24, #22d3ee);
background-size: 280% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: lpGradientMove 7s ease-in-out infinite;
}
.lp-underline {
position: relative;
white-space: nowrap;
}
.lp-underline::after {
content: '';
position: absolute;
left: -2px; right: -2px; bottom: 4px;
height: 10px;
background: linear-gradient(90deg, rgba(34, 211, 238, 0.45), rgba(251, 191, 36, 0.45));
border-radius: 4px;
z-index: -1;
transform: skewX(-8deg);
}
@keyframes lpFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.lp-float { animation: lpFloat 7s ease-in-out infinite; }
@keyframes lpCardIn {
from { opacity: 0; transform: translateX(26px); }
to { opacity: 1; transform: translateX(0); }
}
.lp-card-in { opacity: 0; animation: lpCardIn 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) forwards; }
@keyframes lpBar {
from { height: 0; }
to { height: var(--h); }
}
.lp-bar { height: 0; animation: lpBar 0.8s cubic-bezier(0.2, 0.7, 0.2, 1) forwards; }
@keyframes lpSticker {
from { opacity: 0; transform: scale(0.5) rotate(14deg); }
60% { transform: scale(1.08) rotate(5deg); }
to { opacity: 1; transform: scale(1) rotate(6deg); }
}
.lp-sticker { opacity: 0; animation: lpSticker 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) 1.1s forwards; }
@keyframes lpBlink {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55); }
50% { opacity: 0.6; box-shadow: 0 0 0 5px rgba(52, 211, 153, 0); }
}
.lp-blink { animation: lpBlink 1.6s ease-in-out infinite; }
@keyframes lpMarquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.lp-marquee { animation: lpMarquee 42s linear infinite; }
.lp-marquee-rev { animation: lpMarquee 48s linear infinite reverse; }
.lp-marquee:hover, .lp-marquee-rev:hover { animation-play-state: paused; }
.lp-ticker-mask {
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.lp-reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.lp-reveal.lp-in { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
.lp-blob, .lp-float, .lp-marquee, .lp-marquee-rev, .lp-gradient-text, .lp-blink { animation: none; }
.lp-hero-in, .lp-card-in, .lp-sticker { animation: none; opacity: 1; }
.lp-bar { animation: none; height: var(--h); }
.lp-reveal { opacity: 1; transform: none; transition: none; }
}