Show Telegram status errors and keep setup instructions admin-only

This commit is contained in:
2026-09-06 13:42:25 +03:00
parent 2f86c6d690
commit 44c9809deb
4 changed files with 59 additions and 16 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"date": "2026-09-06",
"environment": "localhost:5183 normal user, injected GET 503 then real GET",
"checks": {
"errorVisible": true,
"retryVisible": true,
"recovered": true,
"adminInstructionsHidden": true,
"oldErrorCleared": true
},
"limits": [
"No Telegram message or link request sent",
"Only status error recovery and normal-user configuration view tested",
"Bot pairing challenge and external delivery remain open"
]
}
+7
View File
@@ -924,3 +924,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Usul alanı explicit usul/ihaleUsulu/alimTuru değerlerinden eşleşir; Mal Alımı değerinden Açık İhale tahmini yapılmaz. Kaynakta usul yoksa usule göre filtrelenmiş sonuçta gösterilmez.
- 209 otomatik test ve derleme geçti. 81 il kapsamı, Türkçe normalizasyon, beş ölçütün taşınması, geçersiz bölge ve usul/kategori ayrımı test edildi. Yerel 5 sentetik ihalede bölge filtresi 1 Giresun kaydına daralttı; bölgeyi kaldırınca 5 kayıt geri geldi; yenileme sonrası sol panelden tekrar uygulandı. 4 kontrol qa-saved-filter-application.json içinde.
- İl bazında bölgeleme coğrafi sınırın ilçelerdeki ayrıntısını temsil etmez. Gerçek kaynak usul/şehir doğruluğu ve bütün ölçütlerin birleşik tarayıcı senaryosu ayrıca kabul edilmelidir. Bu işlem bildirim aboneliği oluşturmaz.
### Telegram durum ekranında hata ve rol görünümü — 6 Eylül 2026
- İlk GET hatasında d=null olduğu için bölüm tamamen kayboluyordu. Yükleme/hata/yeniden deneme ekranı eklendi; bağlantı durumunun boolean alanları doğrulanır. Sonradan durum yenileme düğmesi de vardır.
- Hesap/token anahtarıyla panel ayrılır; kapalı/eski panelin yanıtı UI'ye uygulanmaz. Tek işlem kilidi, duruma göre başarı/hata mesajları ve erişilebilir status/alert rolleri eklendi. Yenileme başarısızlığı yutulup başarı gibi sunulmaz.
- Bot kurulum talimatları yalnız yöneticide gösterilir. Normal kullanıcı hizmetin etkin olmadığını ve yöneticiyle görüşmesi gerektiğini görür. Bağlama başarısı mesajının dış onay mesajını gönderildi varsayması kaldırıldı.
- 5183 yeni izole QA örneğinde ilk GET 503, ardından gerçek API yanıtıyla kurtarma sınandı. 5 kontrol geçti: hata, retry, kurtarma, normal kullanıcıda kurulum ayrıntılarının gizli olması ve hatanın temizlenmesi. Kanıt qa-telegram-status.json. Dış mesaj veya bağlama isteği gönderilmedi.
- Derleme geçti. Sunucu iş mantığı değişmediği için önceki 209 otomatik test tekrar çalıştırılmadı. Bot eşleme kodunun yaşam döngüsü ve gerçek teslim kabulü açık.
+32 -16
View File
@@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react';
import {useApp} from '../../context/AppContext';
import {getToken} from '../../services/auth';
import React, { useEffect, useState, useRef } from 'react';
import { Send, CheckCircle2, AlertTriangle, Loader2, Unplug } from 'lucide-react';
import { telegramDurum, telegramBagla, telegramTest, telegramSil } from '../../services/account';
@@ -10,22 +12,36 @@ import { telegramDurum, telegramBagla, telegramTest, telegramSil } from '../../s
const kodUret = () => String(Math.floor(100000 + Math.random() * 900000));
export const TelegramBaglanti = () => {
const {authUser,isAdmin}=useApp();
return <TelegramPanel key={`${authUser?.email}:${getToken()}`} isAdmin={isAdmin}/>;
};
const TelegramPanel = ({isAdmin}) => {
const [d, setD] = useState(null);
const [kod] = useState(kodUret);
const [busy, setBusy] = useState(null);
const [mesaj, setMesaj] = useState(null);
const yenile = () => telegramDurum().then(setD).catch((e) => setMesaj({ tur: 'hata', metin: e.message }));
useEffect(() => { yenile(); }, []);
const calistir = async (ad, fn, basari) => {
setBusy(ad); setMesaj(null);
try { await fn(); setMesaj({ tur: 'ok', metin: basari }); await yenile(); }
catch (e) { setMesaj({ tur: 'hata', metin: e.message }); }
finally { setBusy(null); }
const alive=useRef(true),locked=useRef(false);
const yenile = async () => {
const data=await telegramDurum();
if(typeof data.bagli!=='boolean'||typeof data.yapilandirilmis!=='boolean')throw new Error('Telegram bağlantı durumu doğrulanamadı.');
if(alive.current)setD(data);
};
const calistir = async (ad, fn, basari) => {
if(locked.current)return;locked.current=true;
setBusy(ad);setMesaj(null);
try {await fn();if(!alive.current)return;if(ad!=='load')await yenile();if(alive.current&&basari)setMesaj({tur:'ok',metin:basari});}
catch(e){if(alive.current)setMesaj({tur:'hata',metin:e.message});}
finally{locked.current=false;if(alive.current)setBusy(null);}
};
useEffect(()=>{alive.current=true;void calistir('load',yenile);return()=>{alive.current=false;};},[]);
if (!d) return null;
if(!d)return <section className="bg-white border rounded-xl p-5 space-y-3" aria-label="Telegram bağlantı durumu">
<h3 className="font-bold text-sm">Telegram Bildirimi</h3>
{busy&&<p role="status" className="text-sm">Bağlantı durumu yükleniyor…</p>}
{mesaj&&<p role="alert" className="text-red-700 text-sm">{mesaj.metin}</p>}
<button disabled={!!busy} onClick={()=>calistir('load',yenile)} className="border rounded p-2 text-sm">Telegram durumunu yeniden dene</button>
</section>;
return (
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
@@ -39,19 +55,18 @@ export const TelegramBaglanti = () => {
</div>
<p className="text-xs text-slate-600 leading-relaxed">
Yeni ihale özetleri, takipteki ihalede <strong>zeyilname/düzeltme ilanı</strong> ve <strong>sonuç</strong>
("X firması Y'ye aldı, %Z kırım") e-postanın yanında Telegram'a da gelir.
Telegram bağlantısı ve ilgili bildirim hizmetleri etkinse ihale özetleri bu kanaldan gönderilebilir. Gönderim, okunma veya her kaynak değişikliğinin yakalanması garanti edilmez.
</p>
{!d.yapilandirilmis ? (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 space-y-2 text-xs text-amber-900 leading-relaxed">
<p className="font-bold">Sunucuda Telegram botu henüz tanımlı değil. Kurulum (yönetici, bir kez):</p>
{isAdmin ? <><p className="font-bold">Sunucuda Telegram botu henüz tanımlı değil. Kurulum (yönetici, bir kez):</p>
<ol className="list-decimal pl-5 space-y-1">
<li>Telegram'da <strong>@BotFather</strong>'a yazın: <code className="bg-amber-100 px-1 rounded">/newbot</code> → bir ad ve <code className="bg-amber-100 px-1 rounded">…bot</code> ile biten kullanıcı adı verin.</li>
<li>BotFather'ın verdiği tokenı Coolify → ihaleist → Environment'a <code className="bg-amber-100 px-1 rounded">TELEGRAM_BOT_TOKEN</code> olarak ekleyin, Redeploy.</li>
<li>Bu sayfa yeniden açıldığında aşağıdaki bağlama adımları görünür.</li>
</ol>
<p className="text-[11px] text-amber-800">Token yalnızca sunucuda durur; uygulamaya girilmez.</p>
<p className="text-[11px] text-amber-800">Token yalnızca sunucuda durur; uygulamaya girilmez.</p></> : <p>Telegram bildirim hizmeti henüz etkin değil. Kurulum için yöneticinizle görüşün.</p>}
</div>
) : d.bagli ? (
<div className="flex flex-wrap gap-2">
@@ -78,15 +93,16 @@ export const TelegramBaglanti = () => {
<li>Bota şu kodu yazıp gönderin: <code className="bg-white border border-sky-300 px-2 py-0.5 rounded font-mono text-sm font-black tracking-widest">{kod}</code></li>
<li>Sonra buraya dönüp <strong>Doğrula</strong>'ya basın.</li>
</ol>
<button onClick={() => calistir('bagla', () => telegramBagla(kod), 'Bağlandı! Telegram\'a onay mesajı gönderildi.')} disabled={!!busy}
<button onClick={() => calistir('bagla', () => telegramBagla(kod), 'Telegram bağlantısı kaydedildi.')} disabled={!!busy}
className="bg-sky-600 hover:bg-sky-500 disabled:opacity-40 text-white font-bold text-xs px-5 py-2 rounded-lg flex items-center gap-1.5">
{busy === 'bagla' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <CheckCircle2 className="w-3.5 h-3.5" />} Doğrula
</button>
</div>
)}
<button disabled={!!busy} onClick={()=>calistir('load',yenile)} className="border rounded p-2 text-xs">Telegram durumunu yenile</button>
{mesaj && (
<div className={`rounded-lg p-3 flex gap-2 text-xs ${mesaj.tur === 'ok' ? 'bg-emerald-50 border border-emerald-200 text-emerald-900' : 'bg-red-50 border border-red-200 text-red-800'}`}>
<div role={mesaj.tur === 'ok' ? 'status' : 'alert'} className={`rounded-lg p-3 flex gap-2 text-xs ${mesaj.tur === 'ok' ? 'bg-emerald-50 border border-emerald-200 text-emerald-900' : 'bg-red-50 border border-red-200 text-red-800'}`}>
{mesaj.tur === 'ok' ? <CheckCircle2 className="w-4 h-4 shrink-0" /> : <AlertTriangle className="w-4 h-4 shrink-0" />}
<span>{mesaj.metin}</span>
</div>
+4
View File
@@ -39,7 +39,11 @@ const qaDelivery=qaSubscriptionStore.reserveDelivery(qaReviewSub.id,{to:user.ema
qaSubscriptionStore.failDelivery(qaDelivery,'Sentetik belirsiz sağlayıcı sonucu.');
qaSubscriptionStore.reserveDelivery(qaReviewSub.id,null,[]);
let subscriptionFailure='read';
let telegramFailure=true;
const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use(async(req,res,next)=>{
if(req.url==='/qa-telegram-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel Telegram testi</h1><button id="recover">Telegram durum hatasını kapat</button><p role="status"></p><script>document.getElementById('recover').onclick=async()=>{await fetch('/qa-telegram-recover',{method:'POST'});document.querySelector('[role=status]').textContent='Hata kapatıldı';};</script></html>`);return;}
if(req.url==='/qa-telegram-recover'&&req.method==='POST'){telegramFailure=false;res.end('ok');return;}
if(req.url==='/api/me/telegram'&&req.method==='GET'&&telegramFailure){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Telegram durumu alınamadı.'}));return;}
if(req.url==='/qa-subscription-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel abonelik testi</h1><button id="read">Abonelik liste hatası</button><button id="delete">Abonelik silme hatası</button><button id="none">Abonelik hatalarını kapat</button><p role="status"></p><script>for(const mode of ['read','delete','none'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-subscription-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
if(req.url?.startsWith('/qa-subscription-failure?')&&req.method==='POST'){subscriptionFailure=new URL(req.url,'http://localhost').searchParams.get('mode');res.end('Abonelik test durumu: '+subscriptionFailure);return;}
if(req.url?.startsWith('/api/notify/subscriptions')&&((subscriptionFailure==='read'&&req.method==='GET')||(subscriptionFailure==='delete'&&req.method==='DELETE'))){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Abonelik deposuna erişilemiyor.'}));return;}