fix(web): error/404 экраны, сплит бандла и bulk-мутации

- RouteErrorComponent/RouteNotFoundComponent на root-маршруте (RU-копирайт,
  retry + переход на главную; redirect в портал не мигает ошибкой)
- settings: форма инициализируется один раз — фоновый refetch больше не
  затирает ввод пользователя
- code splitting: autoCodeSplitting роутов + manualChunks (react/router/query/
  charts/dnd); вход ~507KB вместо единого чанка 1.57MB, recharts (330KB) грузится
  лениво; версия recharts в packages/ui выровнена с apps/web (3.8.0)
- bulk-эндпоинты: POST /agents/approve-bulk и PUT /policy-sets/:id/agents —
  назначение набора агентам одним запросом вместо N×(GET+PUT)
- оптимистичные обновления с rollback: approve, approve-bulk, удаление агента,
  переключение набора
- тесты bulk-операций (45 passed)
This commit is contained in:
Denozordec
2026-09-20 19:12:58 +07:00
parent 40030ce06c
commit 454c5009d1
12 changed files with 418 additions and 129 deletions
+39 -7
View File
@@ -125,21 +125,46 @@ function AgentsPage() {
[navigate],
)
/** Optimistically patch the agents list; returns a rollback fn. */
const patchAgentsCache = useCallback(
(patch: (items: Agent[]) => Agent[]) => {
const prev = qc.getQueryData<{ items: Agent[] }>(['agents'])
if (prev) qc.setQueryData(['agents'], { items: patch(prev.items) })
return () => {
if (prev) qc.setQueryData(['agents'], prev)
}
},
[qc],
)
const approve = useMutation({
mutationFn: (id: string) =>
apiFetch(`/api/v1/agents/${id}/approve`, { method: 'POST' }),
onMutate: (id) =>
patchAgentsCache((items) =>
items.map((a) => (a.id === id ? { ...a, status: 'approved' } : a)),
),
onSuccess: () => {
toast.success('Агент одобрен')
void qc.invalidateQueries({ queryKey: ['agents'] })
},
onError: (e: Error) => toast.error(e.message),
onError: (e: Error, _id, rollback) => {
rollback?.()
toast.error(e.message)
},
})
const approveAllPending = useMutation({
mutationFn: async (ids: string[]) => {
await Promise.all(
ids.map((id) =>
apiFetch(`/api/v1/agents/${id}/approve`, { method: 'POST' }),
mutationFn: (ids: string[]) =>
apiFetch('/api/v1/agents/approve-bulk', {
method: 'POST',
body: JSON.stringify({ agent_ids: ids }),
}),
onMutate: (ids) => {
const target = new Set(ids)
return patchAgentsCache((items) =>
items.map((a) =>
target.has(a.id) ? { ...a, status: 'approved' } : a,
),
)
},
@@ -147,12 +172,16 @@ function AgentsPage() {
toast.success('Все pending одобрены')
void qc.invalidateQueries({ queryKey: ['agents'] })
},
onError: (e: Error) => toast.error(e.message),
onError: (e: Error, _ids, rollback) => {
rollback?.()
toast.error(e.message)
},
})
const removeAgent = useMutation({
mutationFn: (id: string) =>
apiFetch(`/api/v1/agents/${id}`, { method: 'DELETE' }),
onMutate: (id) => patchAgentsCache((items) => items.filter((a) => a.id !== id)),
onSuccess: (_data, id) => {
toast.success('Агент удалён')
setDeleteAgentId(null)
@@ -162,7 +191,10 @@ function AgentsPage() {
void qc.invalidateQueries({ queryKey: ['agents'] })
void qc.invalidateQueries({ queryKey: ['dashboard'] })
},
onError: (e: Error) => toast.error(e.message),
onError: (e: Error, _id, rollback) => {
rollback?.()
toast.error(e.message)
},
})
const items = agentsQ.data?.items ?? []