Simplify: общий загрузчик файлов и единые ключи на фронте

- Четыре копии «ресайз → FormData → CSRF из cookie → fetch → разбор конверта
  ошибки» (аватар, фото партии, фото из админки, иконка ачивки) сведены в
  lib/upload.ts. Иконка по-прежнему уходит оригиналом: ресайз в JPEG убил бы
  прозрачность герба.
- Три копии readCsrfToken и вторая копия resizeImage удалены — берём readCookie
  из api/client.ts и resizeImage из lib/image.ts с параметром размера.
- Ключи, протухающие от партии, собраны в matchAffectedKeys: раньше они были
  написаны строками мимо реестра qk в двух местах, и переименование ключа
  сломало бы инвалидацию молча.
- Различение «нет сессии» и «нет связи» вынесено в authProbeRetry и применено к
  обеим пробам. Прошлый заход чинил только игроцкие гварды, и RequireAdmin
  по-прежнему выкидывал админа на страницу входа при обрыве связи.
- ToastContext больше не пересоздаёт значение контекста: провайдер обёрнут
  вокруг всего приложения, и каждый тост перерисовывал всех потребителей.
- PlaceEditor не трогает DOM, пока цель подсветки не изменилась (было
  querySelectorAll на каждый pointermove).

#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
This commit is contained in:
2026-09-09 18:52:06 +03:00
co-authored by Claude Opus 5
parent 94e2c09952
commit a9de68f27c
11 changed files with 123 additions and 168 deletions
+11 -1
View File
@@ -1,7 +1,7 @@
import createClient from "openapi-fetch";
import type { paths } from "./schema";
function readCookie(name: string): string | null {
export function readCookie(name: string): string | null {
const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
return m ? decodeURIComponent(m[1]) : null;
}
@@ -53,3 +53,13 @@ export function unwrap<T>(res: FetchResult<T>): T {
}
return res.data as T;
}
/**
* Повторы для проб сессии (`/users/me`, `/admin/me`). «Не авторизован» — это
* только ответ сервера (401/403, обрабатывается в самих хуках); обрыв связи
* ответом не является, и без повторов гварды приняли бы его за разлогин и
* увели на страницу входа.
*/
export const authProbeRetry = (count: number, err: unknown): boolean =>
!(err instanceof ApiError) && count < 2;