Files
MathApp_frontend/src/api/ConnectionCheck.tsx
AliReza be9f99d52a add a typed API layer for the Tadrisino backend
اپ تا الان هیچ درخواستی به سرور نمی‌زد و همه‌ی داده‌ها داخلِ خودِ کامپوننت‌ها
هاردکد بود. این کامیت لایه‌ی ارتباط را اضافه می‌کند، بدونِ اینکه هیچ صفحه‌ای
را تغییر دهد.

عمداً فقط فایلِ جدید اضافه شده و App.tsx دست نخورده است، چون کارِ در جریانِ
برنچِ updateApp روی همان فایل است و نباید با هم تصادف کنند. وصل کردنِ
صفحه‌ها به این لایه کارِ بعدی‌ست.

هیچ پکیجِ جدیدی اضافه نشد — فقط fetch استاندارد.

چیزهایی که در client.ts هست برای حالتِ خراب‌اند، نه حالتِ خوب:
  • timeout روی هر درخواست — وگرنه بک‌اندِ کند سوکت‌ها را باز نگه می‌دارد تا
    مرورگر به سقفِ ۶ درخواستِ همزمان بخورد و اپ قفل شود.
  • backoff با jitter — بدونِ jitter همه‌ی کلاینت‌ها بعد از یک خطا همزمان
    دوباره می‌زنند و موجِ دوم از موجِ اول بدتر است.
  • تکرارِ خودکار فقط روی GET؛ POST فقط وقتی Idempotency-Key دارد (ثبتِ پاسخ،
    سفارش) تا دابل‌کلیکِ کاربر دو بار XP یا دو سفارش نسازد.
  • refresh تک‌نسخه‌ای — ۲۰ درخواستِ همزمانِ ۴۰۱ نباید ۲۰ بار refresh بزنند.
  • ادغامِ GETهای یکسانِ در جریان.
  • احترام به هدرِ Retry-After که بک‌اند موقعِ throttle می‌فرستد.

credentials: 'omit' عمدی‌ست: احرازِ هویت توکنِ Bearer در هدر است و سمتِ سرور
CORS_ALLOW_CREDENTIALS=False تنظیم شده.

.env هم به .gitignore اضافه شد؛ قبلاً فقط node_modules داخلش بود و آدرس/
تنظیماتِ محلی می‌توانست ناخواسته کامیت شود.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:06:28 +03:30

88 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* بررسیِ اتصال به بک‌اند — ابزارِ توسعه، نه بخشی از محصول.
*
* برای امتحان کردن، یک خط در App.tsx (یا هر جای دیگر) کافی‌ست:
*
* import { ConnectionCheck } from '@/api/ConnectionCheck';
* <ConnectionCheck />
*
* هیچ فایلِ دیگری را عوض نمی‌کند و اگر بک‌اند بالا نباشد هم اپ را نمی‌خواباند.
*/
import { useEffect, useState } from 'react';
import type { CSSProperties } from 'react';
import { api, ApiError } from '@/api';
import type { Grade } from '@/api';
type State =
| { kind: 'loading' }
| { kind: 'ok'; grades: Grade[] }
| { kind: 'error'; message: string; hint: string };
function hintFor(error: unknown): string {
if (!(error instanceof ApiError)) return 'خطای پیش‌بینی‌نشده — کنسول را ببینید.';
if (error.isNetwork) {
return 'سرور جواب نداد. VITE_API_BASE_URL درست است؟ بک‌اند بالاست؟';
}
if (error.status === 502) {
return 'nginx بالاست ولی جنگو نه — سرویسِ بک‌اند روی سرور اجرا نشده.';
}
if (error.status === 0) return 'ارتباط برقرار نشد.';
if (error.isRateLimited) return 'سقفِ نرخ خورد؛ کلاینت خودش دوباره تلاش می‌کند.';
if (error.isUnauthorized) return 'نیاز به ورود دارد (این اندپوینت نباید داشته باشد).';
return `سرور ${error.status} داد.`;
}
export function ConnectionCheck() {
const [state, setState] = useState<State>({ kind: 'loading' });
useEffect(() => {
const controller = new AbortController();
api
.grades({ signal: controller.signal })
.then((grades) => setState({ kind: 'ok', grades }))
.catch((error: unknown) => {
if (controller.signal.aborted) return;
setState({
kind: 'error',
message: error instanceof Error ? error.message : String(error),
hint: hintFor(error),
});
});
return () => controller.abort();
}, []);
const box: CSSProperties = {
position: 'fixed', bottom: 12, right: 12, zIndex: 9999,
maxWidth: 320, padding: '10px 12px', borderRadius: 10,
font: '12px/1.6 system-ui, sans-serif', direction: 'rtl',
background: '#fff', border: '1px solid #e5e7eb',
boxShadow: '0 4px 16px rgba(0,0,0,.08)',
};
if (state.kind === 'loading') {
return <div style={box}>در حالِ بررسیِ اتصال به بک‌اند…</div>;
}
if (state.kind === 'error') {
return (
<div style={{ ...box, borderColor: '#fecaca' }}>
<b style={{ color: '#b91c1c' }}>اتصال برقرار نشد</b>
<div style={{ marginTop: 4 }}>{state.message}</div>
<div style={{ marginTop: 4, color: '#6b7280' }}>{state.hint}</div>
<div style={{ marginTop: 6, color: '#9ca3af' }}>{api.http.baseUrl}</div>
</div>
);
}
return (
<div style={{ ...box, borderColor: '#bbf7d0' }}>
<b style={{ color: '#15803d' }}>اتصال برقرار است</b>
<div style={{ marginTop: 4 }}>
{state.grades.length} پایه از سرور آمد
{state.grades.length > 0 && `: ${state.grades.map((g) => g.title).join('، ')}`}
</div>
<div style={{ marginTop: 6, color: '#9ca3af' }}>{api.http.baseUrl}</div>
</div>
);
}