"use client" import {useCallback,useEffect,useRef,useState,useSyncExternalStore} from "react" import type {AlertsOutboxOverview} from "@mmapp/contracts/alerts" import {can} from "@/lib/auth" import {getAlertsOutbox,retryAlertsOutbox} from "@/shared/api/alerts-outbox" import {OpsPanel} from "@/components/ops-panel" import {Button} from "@/components/ui/button" import {ApiClientError} from "@/shared/api/http-client" import {outboxPending,subscribeOutboxPending,setOutboxPending} from "@/lib/alerts-outbox-checkpoint" import {AlertDialog,AlertDialogContent,AlertDialogHeader,AlertDialogTitle,AlertDialogDescription,AlertDialogFooter} from "@/components/ui/alert-dialog" export function AlertsOutboxPanel({base,operationsBusy,available,beginOperation,endOperation}:{base:string;operationsBusy:boolean;available:boolean;beginOperation:()=>boolean;endOperation:()=>void}){ const source=JSON.stringify(["live",base]),unknown=useSyncExternalStore(subscribeOutboxPending,()=>outboxPending(source),()=>false) const [selected,setSelected]=useState(null),[ack,setAck]=useState(false),[reviewed,setReviewed]=useState(false),[mutationBusy,setMutationBusy]=useState(false),[mutationError,setMutationError]=useState(null),[message,setMessage]=useState(null) const mutationLock=useRef(false),writeController=useRef(null),writeRevision=useRef(0) const writable=can("mm:alerts:write")&&available&&!operationsBusy&&!unknown&&!mutationBusy const [data,setData]=useState(null),[busy,setBusy]=useState(false),[error,setError]=useState(null),[receivedAt,setReceivedAt]=useState(null) const active=useRef(true),revision=useRef(0),controller=useRef(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,writeGeneration=writeRevision;active.current=true;let pending=true;queueMicrotask(()=>{if(pending)void refresh()});return()=>{pending=false;active.current=false;generation.current++;writeGeneration.current++;controller.current?.abort();writeController.current?.abort()}},[refresh]) async function retry(){ if(!selected||!ack||!writable||mutationLock.current||!beginOperation())return mutationLock.current=true;setMutationBusy(true);setMutationError(null);setMessage(null);controller.current?.abort();revision.current++;setBusy(false) const requestRevision=++writeRevision.current,abort=new AbortController();writeController.current=abort;setOutboxPending(source,true) const current=()=>active.current&&requestRevision===writeRevision.current try{await retryAlertsOutbox(base,selected.id,{expectedVersion:selected.resolutionVersion,acknowledgedDuplicateRisk:true},{signal:abort.signal});if(current()){setOutboxPending(source,false);setSelected(null);setMessage("Сообщение поставлено в очередь. Отправка и доставка ещё не подтверждены.");void refresh()}} catch(e){if(current()){ const rejected=e instanceof ApiClientError&&[400,401,403,409,422].includes(e.status) if(rejected)setOutboxPending(source,false) const detail=e instanceof Error?e.message:"Ответ не получен" setMutationError(rejected?detail:"Результат повтора не подтверждён. Автоматический повтор заблокирован. Проверьте очередь и чат перед новой попыткой. "+detail) }} finally{mutationLock.current=false;endOperation();if(current())setMutationBusy(false)} } return
void refresh()}>Обновить} contentClassName="space-y-3 px-5 py-4"> {error?

{error}{data?". Показан последний успешный снимок очереди.":""}

:null}

{busy?"Обновление очереди…":data?"Последний успешный ответ: "+new Date(receivedAt!).toLocaleString("ru-RU"):"Данные очереди ещё не получены."}

{message?

{message}

:null} {mutationError?

{mutationError}

:null} {unknown?

{mutationBusy?"Постановка в очередь выполняется…":"Результат повтора неизвестен. Автоматический повтор заблокирован. Проверьте очередь и чат: сообщение могло быть отправлено."}

:null} {data?<>

Ожидают: {data.counts.pending} · API принял: {data.counts.sent} · Завершены с ошибкой: {data.counts.failed}

{data.counts.unknown||data.counts.processing||data.counts.unrecognized?

Нужна проверка: неизвестный результат — {data.counts.unknown}, в обработке — {data.counts.processing}, нераспознанный статус — {data.counts.unrecognized}. Эти записи автоматически не переотправляются. Обработка может означать текущий запрос или прерванный процесс отправки; проверьте чат и журнал перед повтором.

:null}

Показано {data.items.length} из {data.counts.processing+data.counts.unknown} удержанных записей. Повтор разрешён только для неизвестного результата после проверки чата.

{data.items.length?
    {data.items.map(item=>
  • {item.ruleName||item.id}

    {item.status==="unknown"?"Результат неизвестен":"Запрос начат, завершение не подтверждено"} · {new Date(item.attemptedAt).toLocaleString("ru-RU")}

    {item.status==="unknown"?:null}
  • )}
:null} :null}
{if(!open&&!mutationLock.current)setSelected(null)}}>Повторить сообщение?{selected?.ruleName||selected?.id}. Предыдущая попытка могла доставить сообщение. Повтор может создать дубликат; повтор использует сохранённое сообщение и настройки подключения Telegram на момент новой попытки.{mutationError?

{mutationError}

:null}
}