From 8a3ae3342ec468fea408995f50941acea96a5fb9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 04:02:31 +0400 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D0=B1:=20=D0=BF=D0=BE=D0=B4=D1=85?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=20refresh=20=D0=BF=D1=80=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=B5=20wiki=5Fsync,=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BA=D0=B0=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D1=80=D0=B0=D0=B7=D1=83=20(M-12,=20M-15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/modules/jira/refresh/RefreshProvider.tsx | 15 +++++++++------ web/src/modules/jira/refresh/refresh.test.tsx | 10 ++++++++++ web/src/modules/wiki/PageView.tsx | 14 ++++++++++---- web/src/modules/wiki/WikiPage.test.tsx | 8 ++++++++ web/src/modules/wiki/api/queries.ts | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/web/src/modules/jira/refresh/RefreshProvider.tsx b/web/src/modules/jira/refresh/RefreshProvider.tsx index 16f5e27..56c8bba7 100644 --- a/web/src/modules/jira/refresh/RefreshProvider.tsx +++ b/web/src/modules/jira/refresh/RefreshProvider.tsx @@ -43,12 +43,15 @@ export function RefreshProvider({ children, pollMs = POLL_MS }: { children: Reac picking.current = true; (async () => { const r = await api.get<{ batches: Batch[] }>("/batches", { limit: 20 }); - const b = r.batches.find((x) => x.author === "viewer" && (x.status === "queued" || x.status === "running")); - if (!b) return; - const full = await api.get(`/batches/${b.id}`); // T10.6: в списке нет команд — тип смотрим в пакете (R-2) - if (full.commands?.[0]?.type !== "refresh" || activeRef.current || startingRef.current) return; - failures.current = 0; - setActive((cur) => cur ?? { id: b.id, agentOffline: false }); + const candidates = r.batches.filter((x) => x.author === "viewer" && (x.status === "queued" || x.status === "running")); + for (const b of candidates) { // M-12: пакет wiki_sync не заслоняет более старый refresh + const full = await api.get(`/batches/${b.id}`); // T10.6: в списке нет команд — тип смотрим в пакете (R-2) + if (activeRef.current || startingRef.current) return; + if (full.commands?.[0]?.type !== "refresh") continue; + failures.current = 0; + setActive((cur) => cur ?? { id: b.id, agentOffline: false }); + return; + } })().catch(() => { /* не критично: подхватим при следующей загрузке */ }).finally(() => { picking.current = false; }); }, []); diff --git a/web/src/modules/jira/refresh/refresh.test.tsx b/web/src/modules/jira/refresh/refresh.test.tsx index 010a216..f117583 100644 --- a/web/src/modules/jira/refresh/refresh.test.tsx +++ b/web/src/modules/jira/refresh/refresh.test.tsx @@ -128,6 +128,16 @@ describe("RefreshProvider", () => { expect(h.count("GET /batches/7")).toBe(1); // только проверка типа, опроса нет }); + it("M-12: пакет wiki_sync viewer не заслоняет более старый refresh", async () => { + hub({ + "GET /batches": () => json(200, { batches: [{ id: 9, author: "viewer", status: "queued" }, { id: 7, author: "viewer", status: "running" }] }), + "GET /batches/:id": (url) => batch("running", { type: url.pathname.endsWith("/9") ? "wiki_sync" : "refresh" }), + }); + const { result } = refreshHook(); + await waitFor(() => expect(result.current.event).toMatchObject({ kind: "batch", id: 7 })); + expect(result.current.busy).toBe(true); + }); + it("401 на запуске пакета с текущим токеном — токен сброшен", async () => { hub({ "GET /agent": () => json(401, { error: "unauthorized" }) }); const { result } = refreshHook(); diff --git a/web/src/modules/wiki/PageView.tsx b/web/src/modules/wiki/PageView.tsx index bce6868..b5298b8 100644 --- a/web/src/modules/wiki/PageView.tsx +++ b/web/src/modules/wiki/PageView.tsx @@ -21,19 +21,21 @@ const decode = (s: string) => { try { return decodeURIComponent(s); } catch { re export function PageView({ project, code, hrefOf }: { project: string; code: string; hrefOf(code: string): string }) { const page = useWikiPage(project, code); const dl = useWikiDownload(project, code); + const atts = page.data?.attachments; + const failed = useMemo(() => new Map((atts ?? []).filter((a) => a.state === "failed" && a.error).map((a) => [String(a.id), a.error!])), [atts]); const components = useMemo(() => ({ a({ href, children }: ComponentProps<"a">) { if (!href) return <>{children}; if (/^page:/i.test(href)) return {children}; - if (/^attachment:/i.test(href)) return {children}; + if (/^attachment:/i.test(href)) return {children}; if (/^https?:/i.test(href)) return {children}; return {children}; }, // картинки по внешним адресам не загружаются (трекинг, чужие ресурсы): виден только текст img: ({ alt }: ComponentProps<"img">) => [изображение{alt ? `: ${alt}` : ""}], table: ({ children }: ComponentProps<"table">) =>
{children}
, - }), [hrefOf, dl]); + }), [hrefOf, dl, failed]); if (page.isPending) return

Загрузка страницы…

; if (page.isError) return {page.error.message}; @@ -57,8 +59,12 @@ export function PageView({ project, code, hrefOf }: { project: string; code: str } // ссылка attachment:: запрос на скачивание, ожидание, сохранение файла; ошибка — текстом рядом -function AttachmentLink({ id, dl, children }: { id: string; dl: ReturnType; children?: ReactNode }) { - const { busy, error } = dl.status(id); +// failed — ошибки вложений в состоянии failed из ответа страницы: показываются сразу, до клика +function AttachmentLink({ id, dl, failed, children }: { + id: string; dl: ReturnType; failed: Map; children?: ReactNode; +}) { + const st = dl.status(id); + const busy = st.busy, error = st.error ?? (busy ? null : failed.get(id) ?? null); return ( <>