feat: İhaleAB'den taşınan özellikler — MCP sunucusu, RSS, haftalık brief, teklif asistanı, yeterlilik rozeti, 5 yeni kırmızı bayrak

- /mcp (JSON-RPC, ih_live_ anahtarı): search_tenders, get_tender, search_results, search_items, upcoming_deadlines
- /api/feeds/tenders.rss (anahtarlı) ve /api/feeds/sonuc.rss (açık)
- Haftalık brief (Pazartesi 08:00, Telegram + e-posta), /api/me/brief tercihi + önizleme, sağ panelde yaklaşan ihale tarihleri
- Detayda Teklif Asistanı sekmesi: belgeye sor (atıflı), taslak denetimi, taslak üretimi (analyzeSpec zinciri)
- Derin analiz belge listesi × Doküman Deposu eşleştirme rozeti
- Kırmızı bayrak: kısa süre, hafta sonu tarih, geç sözleşme, kazananı gizli, DT parçalama şüphesi
- Kullanım kılavuzu güncellendi

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 02:23:41 +03:00
co-authored by Claude Fable 5.1
parent fabff18fe9
commit 48f0c48732
18 changed files with 940 additions and 10 deletions
+3
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { listApiKeys, createApiKey, revokeApiKey } from '../../services/account';
import { Webhook, Key, Plus, Trash2, Loader2, Copy, Check, AlertTriangle, ShieldCheck, Lock } from 'lucide-react';
import { McpRssKarti } from './McpRssKarti';
/**
* Gerçek API anahtarı yönetimi.
@@ -154,6 +155,8 @@ export const ApiWebhooks = () => {
)}
</div>
<McpRssKarti />
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p className="text-[11px] text-slate-600 leading-relaxed">
<strong>Kullanım:</strong> İsteklerinize <code className="bg-white px-1 rounded border">Authorization: Bearer ih_live_…</code>
+48
View File
@@ -0,0 +1,48 @@
import React, { useEffect, useState } from 'react';
import { CalendarClock, Eye, Loader2 } from 'lucide-react';
import { getToken } from '../../services/auth';
/**
* Haftalık brief tercihi (Pazartesi 08:00): bu hafta yapılacak takipli
* ihaleler + geçen hafta sonuçlanan takipliler — Telegram ve e-posta.
* "Önizle" şu anki içeriği gösterir; anahtar kapatır.
*/
const iste = (opts = {}) => fetch('/api/me/brief', { headers: { Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' }, ...opts }).then((r) => r.json());
export const BriefTercihi = () => {
const [kapali, setKapali] = useState(false);
const [metin, setMetin] = useState(null);
const [mesgul, setMesgul] = useState(false);
useEffect(() => { iste().then((d) => { if (d?.success) setKapali(!!d.kapali); }).catch(() => {}); }, []);
const degistir = async () => {
const yeni = !kapali; setKapali(yeni);
try { await iste({ method: 'POST', body: JSON.stringify({ kapali: yeni }) }); } catch { setKapali(!yeni); }
};
const onizle = async () => {
setMesgul(true);
try { const d = await iste(); setMetin(d?.metin || 'Önizleme alınamadı.'); } catch { setMetin('Önizleme alınamadı.'); } finally { setMesgul(false); }
};
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-3">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2"><CalendarClock className="w-4 h-4 text-amber-500" /> Haftalık Brief</h3>
<p className="text-[11px] text-slate-500 mt-1">Her Pazartesi 08:00: bu hafta yapılacak takipli ihaleler ve geçen hafta sonuçlanan takipliler — Telegram (bağlıysa) ve e-posta ile tek mesaj.</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={onizle} disabled={mesgul} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">
{mesgul ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />} Önizle
</button>
<button role="switch" aria-checked={!kapali} onClick={degistir}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition ${kapali ? 'bg-slate-300' : 'bg-emerald-500'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${kapali ? 'translate-x-0.5' : 'translate-x-4'}`} />
</button>
</div>
</div>
{metin && <pre className="whitespace-pre-wrap rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700 font-sans leading-relaxed">{metin}</pre>}
</div>
);
};
+42
View File
@@ -0,0 +1,42 @@
import React, { useState } from 'react';
import { Plug, Rss, Copy, Check } from 'lucide-react';
/** MCP sunucusu + RSS besleme kartı (API & Webhooks sekmesi). */
export const McpRssKarti = () => {
const origin = typeof window !== 'undefined' ? window.location.origin : 'https://ihaleist.com';
const url = `${origin}/mcp`;
const anahtar = 'ih_live_ANAHTARINIZ';
const [kopya, setKopya] = useState(null);
const kopyala = async (k, t) => { try { await navigator.clipboard.writeText(t); setKopya(k); setTimeout(() => setKopya(null), 1500); } catch { /* yok */ } };
const ornekler = [
{ k: 'cc', ad: 'Claude Code', not: 'terminalde tek komut', kod: `claude mcp add --transport http ihaleist ${url} --header "Authorization: Bearer ${anahtar}"` },
{ k: 'cursor', ad: 'Cursor / Windsurf', not: '.cursor/mcp.json', kod: JSON.stringify({ mcpServers: { ihaleist: { url, headers: { Authorization: `Bearer ${anahtar}` } } } }, null, 2) },
{ k: 'desktop', ad: 'Claude Desktop', not: 'claude_desktop_config.json (mcp-remote)', kod: JSON.stringify({ mcpServers: { ihaleist: { command: 'npx', args: ['-y', 'mcp-remote', url, '--header', `Authorization: Bearer ${anahtar}`] } } }, null, 2) },
];
const rss = `${origin}/api/feeds/tenders.rss?key=${anahtar}&il=İstanbul&q=eldiven`;
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-4">
<div className="border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2"><Plug className="w-4 h-4 text-violet-600" /> MCP Sunucusu — yapay zekâ araçlarınıza İhaleist'i bağlayın</h3>
<p className="text-[11px] text-slate-500 mt-1">Claude Code, Cursor, Claude Desktop gibi MCP destekli araçlar İhaleist'i doğrudan sorgular: <code>search_tenders</code>, <code>get_tender</code>, <code>search_results</code>, <code>search_items</code>, <code>upcoming_deadlines</code>. Yukarıdaki API anahtarınızı kullanır.</p>
</div>
{ornekler.map((o) => (
<div key={o.k} className="space-y-1">
<div className="flex items-center justify-between">
<p className="text-xs font-bold text-slate-800">{o.ad} <span className="font-normal text-slate-500">— {o.not}</span></p>
<button onClick={() => kopyala(o.k, o.kod)} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">{kopya === o.k ? <Check className="w-3 h-3 text-emerald-600" /> : <Copy className="w-3 h-3" />} Kopyala</button>
</div>
<pre className="overflow-x-auto rounded-lg bg-slate-900 text-slate-100 p-2 text-[11px]">{o.kod}</pre>
</div>
))}
<div className="border-t border-slate-100 pt-3 space-y-1">
<div className="flex items-center justify-between">
<p className="text-xs font-bold text-slate-800 flex items-center gap-1"><Rss className="w-3.5 h-3.5 text-orange-500" /> RSS beslemesi <span className="font-normal text-slate-500">— Feedly, Outlook, Slack RSS, n8n… (il, sektor, q filtreleri)</span></p>
<button onClick={() => kopyala('rss', rss)} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">{kopya === 'rss' ? <Check className="w-3 h-3 text-emerald-600" /> : <Copy className="w-3 h-3" />} Kopyala</button>
</div>
<pre className="overflow-x-auto rounded-lg bg-slate-900 text-slate-100 p-2 text-[11px]">{rss}</pre>
<p className="text-[11px] text-slate-500">Sonuç ilanları beslemesi anahtarsız: <code>{origin}/api/feeds/sonuc.rss</code> (firma, idare, il filtreleri). Anahtarı <code>ih_live_ANAHTARINIZ</code> yerine yazın.</p>
</div>
</div>
);
};
+3 -1
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { TelegramBaglanti } from './TelegramBaglanti';
import { BriefTercihi } from './BriefTercihi';
import { getToken } from '../../services/auth';
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
@@ -108,7 +109,8 @@ export const NotifySubscriptions = () => {
return (
<>
<TelegramBaglanti />
<BriefTercihi />
<TelegramBaglanti />
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-5">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
+6 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import { useApp } from '../../context/AppContext';
import { redFlags, marketConcentration, formatTL } from '../../services/analytics';
import { ShieldAlert, AlertTriangle, TrendingDown, Building2, Users, Info } from 'lucide-react';
import { ShieldAlert, AlertTriangle, TrendingDown, Building2, Users, Info, Clock, CalendarX, Hourglass, EyeOff, Scissors } from 'lucide-react';
const SEVIYE = {
yuksek: { etiket: 'Yüksek', kutu: 'bg-red-50 border-red-200', rozet: 'bg-red-100 text-red-800', cizgi: 'bg-red-500' },
@@ -15,6 +15,11 @@ const TUR_IKON = {
'Tahmin üstü bedel': AlertTriangle,
'Aşırı düşük teklif': TrendingDown,
'Tedarikçi yoğunlaşması': Building2,
'Kısa süre': Clock,
'Hafta sonu tarih': CalendarX,
'Geç sözleşme': Hourglass,
'Kazananı gizli': EyeOff,
'Parçalama şüphesi': Scissors,
};
export const RedFlags = () => {