feat: takip izleme (zeyilname + sonuc bildirimi), hazirlik gorevi, belge sure uyarisi, Telegram, OCR, yasakli sorgu

28 depo taramasindan alinan alti ozellik. Kod kopyalanmadi (depolar
lisanssiz), fikirler uygulandi.

1) Zeyilname / duzeltme ilani takibi (server/zeyilname.mjs): takipteki
   IKN'ler sunucuya yazilir; 3 saatte bir EKAP detayindan ilan sayisi ve
   ihale tarihi karsilastirilir, degisince "ne degisti" bildirimi (uygulama
   ici + e-posta + Telegram). Test: anlik kaydi eskitilince tur "yeni ilan
   yayimlandi; ihale tarihi 01.01.2026 → 08.10.2026" bildirimi uretti.
2) Sonuc bildirimi: takipteki ihale sonuclaninca "X firmasi Y'ye aldi, %Z
   kirim". Yalniz takiptekiler; kismi ihalede oran yazilmaz.
3) Ihaleden goreve (MOBIT-DURT): detayda "Hazirlik Gorevi Ac" → teslim
   tarihi ihale saati, pano "Teklif Hazirlaniyor", derin analizden gelen
   belge kontrol listesi gorevde; Kanban kartinda geri sayim rozeti.
4) Sirket belgesi sure takibi: Belge Kasasi'nda son kullanma tarihi girisi,
   dolmus / 30 gun icinde dolacak uyarisi; girise gunde bir bildirim.
   PATCH /api/me/documents/meta.
5) Telegram (server/telegram.mjs): TELEGRAM_BOT_TOKEN sunucuda; kullanici
   6 haneli kodu bota yazar, "Dogrula" → getUpdates ile chat id baglanir.
   Arayuzde adim adim kurulum rehberi (BotFather → env → Redeploy → kod).
   Ihale ozeti, zeyilname ve sonuc bildirimleri Telegram'a da gider.
6) OCR (tesseract.js, tur): PDF metin katmani bossa (taranmis) ilk 8 sayfa
   OCR. Olcum: sentetik cetvelde guven 87, ~0,3 sn/sayfa; IKN ve kalemler
   dogru okundu. Kalem havuzu ve derin analiz artik taranmis PDF'ten de
   besleniyor.
7) Rakip Radari'nda KIK yasakli sorgusu baglantisi (firma adi panoya
   kopyalanir; EKAP sorguyu API'den vermiyor).

Sinir deger hesaplayici KURULMADI: Kamu Ihale Genel Tebligi'ne ulasilamadi
(mevzuat.gov.tr / ihale.gov.tr bu makineden erisilemiyor); formul kaynaksiz
yazilmadi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 22:58:13 +03:00
co-authored by Claude Fable 5.1
parent 1716b6365c
commit df89eaac33
18 changed files with 802 additions and 10 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Telegram bildirim kanalı — Bot API, bağımlılık yok.
*
* KURULUM (kullanıcı/yönetici): BotFather'da bot oluşturulur, tokenı
* TELEGRAM_BOT_TOKEN env'ine girilir. Her kullanıcı uygulamadan aldığı 6 haneli
* kodu bota yazar; sunucu getUpdates ile kodu görüp chat id'yi kullanıcıya
* bağlar. Token bizde tutulmaz, sohbet kimliği kullanıcı verisinde durur.
*/
const API = () => `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;
export function telegramConfigured() {
return Boolean(process.env.TELEGRAM_BOT_TOKEN);
}
let botOnbellek = { ad: null, zaman: 0 };
/** Bot kullanıcı adı (t.me/<ad>) — arayüzde bağlantı vermek için. */
export async function botAdi() {
if (!telegramConfigured()) return null;
if (botOnbellek.ad && Date.now() - botOnbellek.zaman < 3600e3) return botOnbellek.ad;
try {
const r = await fetch(`${API()}/getMe`, { signal: AbortSignal.timeout(10000) });
const d = await r.json();
if (d.ok) { botOnbellek = { ad: d.result.username, zaman: Date.now() }; return d.result.username; }
} catch { /* aşağıda null */ }
return null;
}
export async function sendTelegram(chatId, text) {
if (!telegramConfigured() || !chatId) return false;
const r = await fetch(`${API()}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, text: String(text).slice(0, 4000), parse_mode: 'HTML', disable_web_page_preview: true }),
signal: AbortSignal.timeout(15000),
});
const d = await r.json().catch(() => ({}));
if (!d.ok) throw new Error(d.description || `Telegram HTTP ${r.status}`);
return true;
}
/**
* Kullanıcının bota yazdığı 6 haneli kodu son güncellemelerde arar.
* Bulursa { chatId, ad } döner. Long-polling yok; kullanıcı "doğrula" dediğinde
* tek getUpdates çağrısı yeterli (Telegram son 24 saati tutar).
*/
export async function kodIleBagla(kod) {
if (!telegramConfigured()) throw new Error('Sunucuda TELEGRAM_BOT_TOKEN tanımlı değil');
const r = await fetch(`${API()}/getUpdates?limit=100`, { signal: AbortSignal.timeout(15000) });
const d = await r.json().catch(() => ({}));
if (!d.ok) throw new Error(d.description || 'getUpdates başarısız');
const aranan = String(kod).trim();
for (const u of [...(d.result || [])].reverse()) {
const m = u.message || u.edited_message;
if (!m?.text) continue;
if (m.text.replace(/\D/g, '').includes(aranan)) {
return { chatId: String(m.chat.id), ad: [m.chat.first_name, m.chat.last_name].filter(Boolean).join(' ') || m.chat.username || null };
}
}
return null;
}
/** HTML kaçışı — ihale adları '<' '&' içerebiliyor. */
export const tgKacir = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));