fix: 3 bugs found in audit

1. _xlsx_auth_manager: возвращал (tg_id, user) при успехе → callers делали
   `if err: return err` и возвращали dict пользователя вместо данных.
   Исправлено: возвращает (tg_id, None) при успешной авторизации.

2. Promise.all с 4 запросами: ошибка Drive (сервис-аккаунт не добавлен к файлу)
   роняла весь дашборд. Исправлено: складские запросы изолированы в отдельный
   .then/.catch — дашборд замеров отрисовывается независимо.

3. Секции склада теперь появляются с задержкой (non-blocking), а не блокируют
   отрисовку главного экрана.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-16 08:28:18 +03:00
parent e97c84e126
commit 1b8f70e44a
3 changed files with 28 additions and 23 deletions

View File

@ -3140,7 +3140,7 @@ def _initial(name: str) -> str:
def _xlsx_auth_manager(body: dict[str, Any]) -> tuple[Any, dict[str, Any] | None]: def _xlsx_auth_manager(body: dict[str, Any]) -> tuple[Any, dict[str, Any] | None]:
"""Проверяет initData и возвращает (tg_id, user) для менеджера или (None, error_dict).""" """Проверяет initData и возвращает (tg_id, None) для менеджера или (None, error_dict)."""
cfg = get_config() cfg = get_config()
auth = verify_init_data(body.get("initData") or "", cfg.bot_token) auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
if not auth or not auth.get("user"): if not auth or not auth.get("user"):
@ -3153,7 +3153,7 @@ def _xlsx_auth_manager(body: dict[str, Any]) -> tuple[Any, dict[str, Any] | None
user = sheets.find_user(tg_id) user = sheets.find_user(tg_id)
if not user or not sheets.has_role(user, "manager"): if not user or not sheets.has_role(user, "manager"):
return None, {"error": "only_manager"} return None, {"error": "only_manager"}
return tg_id, user return tg_id, None # успешно: вернуть None-ошибку, чтобы caller мог сделать if err:
def _parse_xlsx_groups(file_bytes: bytes, source_label: str) -> list[dict[str, Any]]: def _parse_xlsx_groups(file_bytes: bytes, source_label: str) -> list[dict[str, Any]]:

View File

@ -184,25 +184,30 @@ async function renderManagerHome(me) {
const pendingContainer = el(`<div id="pendingContainer"></div>`); const pendingContainer = el(`<div id="pendingContainer"></div>`);
app.insertBefore(pendingContainer, todayContainer); app.insertBefore(pendingContainer, todayContainer);
// Параллельно грузим реальные данные (измерения + pending + отгрузки + поступления) // Параллельно грузим реальные данные (измерения + pending — критичные)
// Складские данные грузим отдельно, чтобы ошибка Drive не ломала весь дашборд
try { try {
const authBody = { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null }; const authBody = { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null };
const [resM, resP, resS, resA] = await Promise.all([ const [resM, resP] = await Promise.all([
fetch(`${BACKEND_URL}/api/measurements`, { method: "POST", body: JSON.stringify(authBody) }), fetch(`${BACKEND_URL}/api/measurements`, { method: "POST", body: JSON.stringify(authBody) }),
fetch(`${BACKEND_URL}/api/manager_pending`, { method: "POST", body: JSON.stringify(authBody) }), fetch(`${BACKEND_URL}/api/manager_pending`, { method: "POST", body: JSON.stringify(authBody) }),
fetch(`${BACKEND_URL}/api/shipments`, { method: "POST", body: JSON.stringify(authBody) }),
fetch(`${BACKEND_URL}/api/arrivals`, { method: "POST", body: JSON.stringify(authBody) }),
]); ]);
const data = await resM.json(); const data = await resM.json();
const pendingData = await resP.json(); const pendingData = await resP.json();
const shipmentsData = await resS.json();
const arrivalsData = await resA.json();
renderManagerPending(pendingContainer, pendingData.pending || []); renderManagerPending(pendingContainer, pendingData.pending || []);
renderManagerToday(todayContainer, data.measurements || [], firstName, greetingEl); renderManagerToday(todayContainer, data.measurements || [], firstName, greetingEl);
renderManagerProjects(projectsContainer, data.measurements || []); renderManagerProjects(projectsContainer, data.measurements || []);
renderManagerShipments(shipmentsContainer, shipmentsData.shipments || [], "📦 Отгрузки с завода");
renderManagerShipments(arrivalsContainer, arrivalsData.shipments || [], "📥 Поступление в СПб"); // Складские данные — не критичны; грузим после, ошибка не ломает дашборд
const authBodyStr = JSON.stringify(authBody);
Promise.all([
fetch(`${BACKEND_URL}/api/shipments`, { method: "POST", body: authBodyStr }).then(r => r.json()).catch(() => ({})),
fetch(`${BACKEND_URL}/api/arrivals`, { method: "POST", body: authBodyStr }).then(r => r.json()).catch(() => ({})),
]).then(([shipmentsData, arrivalsData]) => {
renderManagerShipments(shipmentsContainer, shipmentsData.shipments || [], "📦 Отгрузки с завода");
renderManagerShipments(arrivalsContainer, arrivalsData.shipments || [], "📥 Поступление в СПб");
}).catch(() => { /* тихо — дашборд уже отрисован */ });
} catch (e) { } catch (e) {
todayContainer.innerHTML = `<div class="error">Не удалось загрузить данные: ${escHtml(e.message)}</div>`; todayContainer.innerHTML = `<div class="error">Не удалось загрузить данные: ${escHtml(e.message)}</div>`;
} }

View File

@ -12,14 +12,14 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap">
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260516c"> <link rel="stylesheet" href="assets/styles.css?v=20260516d">
<link rel="stylesheet" href="assets/podbor.css?v=20260516c"> <link rel="stylesheet" href="assets/podbor.css?v=20260516d">
</head> </head>
<body> <body>
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск --> <!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
<div class="loader splash" id="splash"> <div class="loader splash" id="splash">
<div class="brand-logo-wrap"> <div class="brand-logo-wrap">
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260516c" alt="@wasrusgen1"> <img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260516d" alt="@wasrusgen1">
<div class="splash-dust" aria-hidden="true"> <div class="splash-dust" aria-hidden="true">
<span class="dust d1"></span> <span class="dust d2"></span> <span class="dust d1"></span> <span class="dust d2"></span>
<span class="dust d3"></span> <span class="dust d4"></span> <span class="dust d3"></span> <span class="dust d4"></span>
@ -35,15 +35,15 @@
<div class="brand-tagline-gold">CRM</div> <div class="brand-tagline-gold">CRM</div>
</div> </div>
<main id="app"></main> <main id="app"></main>
<script src="assets/icons.js?v=20260516c"></script> <script src="assets/icons.js?v=20260516d"></script>
<script src="assets/podbor.config.js?v=20260516c"></script> <script src="assets/podbor.config.js?v=20260516d"></script>
<script src="assets/podbor.picts.js?v=20260516c"></script> <script src="assets/podbor.picts.js?v=20260516d"></script>
<script src="assets/podbor.js?v=20260516c"></script> <script src="assets/podbor.js?v=20260516d"></script>
<script src="assets/clients.js?v=20260516c"></script> <script src="assets/clients.js?v=20260516d"></script>
<script src="assets/zamer-picts.js?v=20260516c"></script> <script src="assets/zamer-picts.js?v=20260516d"></script>
<script src="assets/measurements.js?v=20260516c"></script> <script src="assets/measurements.js?v=20260516d"></script>
<script src="assets/request.js?v=20260516c"></script> <script src="assets/request.js?v=20260516d"></script>
<script src="assets/assembly.js?v=20260516c"></script> <script src="assets/assembly.js?v=20260516d"></script>
<script src="assets/app.js?v=20260516c"></script> <script src="assets/app.js?v=20260516d"></script>
</body> </body>
</html> </html>