30 lines
3.9 KiB
TypeScript
30 lines
3.9 KiB
TypeScript
"use client"
|
||
import {useCallback,useEffect,useRef,useState} from "react"
|
||
import type {AlertsOutboxOverview} from "@mmapp/contracts/alerts"
|
||
import {can} from "@/lib/auth"
|
||
import {getAlertsOutbox} from "@/shared/api/alerts-outbox"
|
||
import {OpsPanel} from "@/components/ops-panel"
|
||
import {Button} from "@/components/ui/button"
|
||
export function AlertsOutboxPanel({base}:{base:string}){
|
||
const [data,setData]=useState<AlertsOutboxOverview|null>(null),[busy,setBusy]=useState(false),[error,setError]=useState<string|null>(null),[receivedAt,setReceivedAt]=useState<string|null>(null)
|
||
const active=useRef(true),revision=useRef(0),controller=useRef<AbortController|null>(null)
|
||
const refresh=useCallback(async()=>{
|
||
if(!active.current||!can("mm:alerts:read"))return
|
||
controller.current?.abort();controller.current=new AbortController();const requestRevision=++revision.current;setBusy(true);setError(null)
|
||
const current=()=>active.current&&requestRevision===revision.current&&can("mm:alerts:read")
|
||
try{const result=await getAlertsOutbox(base,{signal:controller.current.signal});if(current()){setData(result);setReceivedAt(new Date().toISOString())}}catch(e){if(current())setError(e instanceof Error?e.message:"Не удалось получить очередь")}
|
||
finally{if(current())setBusy(false)}
|
||
},[base])
|
||
useEffect(()=>{const generation=revision;active.current=true;let pending=true;queueMicrotask(()=>{if(pending)void refresh()});return()=>{pending=false;active.current=false;generation.current++;controller.current?.abort()}},[refresh])
|
||
return <section aria-label="Очередь Telegram" className="min-w-0"><OpsPanel title="Очередь Telegram" description="Приём сообщения API и доставка в чат — разные состояния." headerRight={<Button type="button" variant="outline" size="sm" aria-label="Обновить очередь Telegram" disabled={busy} onClick={()=>void refresh()}>Обновить</Button>} contentClassName="space-y-3 px-5 py-4">
|
||
{error?<p role="alert" className="text-xs text-destructive break-words">{error}{data?". Показан последний успешный снимок очереди.":""}</p>:null}
|
||
<p role="status" className="text-xs text-muted-foreground">{busy?"Обновление очереди…":data?"Последний успешный ответ: "+new Date(receivedAt!).toLocaleString("ru-RU"):"Данные очереди ещё не получены."}</p>
|
||
{data?<>
|
||
<p className="text-sm">Ожидают: {data.counts.pending} · API принял: {data.counts.sent} · Завершены с ошибкой: {data.counts.failed}</p>
|
||
{data.counts.unknown||data.counts.processing||data.counts.unrecognized?<p role="status" className="text-xs break-words">Нужна проверка: неизвестный результат — {data.counts.unknown}, в обработке — {data.counts.processing}, нераспознанный статус — {data.counts.unrecognized}. Эти записи автоматически не переотправляются. Обработка может означать текущий запрос или прерванный процесс отправки; проверьте чат и журнал перед повтором.</p>:null}
|
||
<p className="text-xs text-muted-foreground">Показано {data.items.length} из {data.counts.processing+data.counts.unknown} удержанных записей. Повторная отправка из этого экрана пока недоступна.</p>
|
||
{data.items.length?<ul className="space-y-2">{data.items.map(item=><li key={item.id} className="break-words text-xs"><p className="font-medium">{item.ruleName||item.id}</p><p className="text-muted-foreground">{item.status==="unknown"?"Результат неизвестен":"Запрос начат, завершение не подтверждено"} · {new Date(item.attemptedAt).toLocaleString("ru-RU")}</p></li>)}</ul>:null}
|
||
</>:null}
|
||
</OpsPanel></section>
|
||
}
|