feat: migrate app state to bootstrap endpoint
app-ci-deploy / test-build-deploy (push) Has been cancelled

Use GET /me/bootstrap as the primary authenticated state source per ADR-0003. AuthContext now fetches and exposes typed bootstrapData, Header and class/detail admin checks use bootstrap permissions/roles, dashboard surfaces onboarding/calendar/timetable/graph status, and graph navigation derives school setup state from bootstrap instead of /school/status.

Refs: t_44353587
This commit is contained in:
2026-05-28 19:07:07 +01:00
parent 67e47fc47f
commit 65ce1bede8
6 changed files with 270 additions and 81 deletions
@@ -483,11 +483,10 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
// ─── Main Panel ───────────────────────────────────────────────────────────────
export function CCGraphNavPanel() {
const { accessToken } = useAuth();
const { accessToken, bootstrapData } = useAuth();
const navigate = useNavigate();
const { navigateToNeoNode, context } = useNavigationStore();
const [tree, setTree] = useState<TreeNode | null>(null);
const [schoolStatus, setSchoolStatus] = useState<SchoolStatus | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -526,19 +525,31 @@ export function CCGraphNavPanel() {
}
}, [accessToken, apiBase]);
const fetchSchoolStatus = useCallback(async () => {
if (!accessToken) return;
try {
const res = await fetch(`${apiBase}/school/status`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) return;
const data = await res.json();
setSchoolStatus(data);
} catch {
// non-fatal — panel still works without school status
}
}, [accessToken, apiBase]);
const schoolStatus = useMemo<SchoolStatus | null>(() => {
if (!bootstrapData) return null;
const membershipRole = bootstrapData.active_institute?.membership_role ?? undefined;
const activeMembership = bootstrapData.memberships?.find(
(m) => m.institute_id === bootstrapData.active_institute?.id && m.is_active
);
return {
status: bootstrapData.school_status,
user_role: membershipRole,
school_id: bootstrapData.active_institute?.id ?? undefined,
school_has_calendar: bootstrapData.calendar_status.available && !bootstrapData.calendar_status.needs_setup,
teacher_has_timetable: bootstrapData.timetable_status.available && !bootstrapData.timetable_status.needs_setup,
timetable_id: bootstrapData.timetable_status.teacher_timetable_id,
periods_template: [],
school_info: activeMembership?.institute ? {
name: activeMembership.institute.name,
urn: activeMembership.institute.urn ?? '',
website: '',
address: {},
headteacher: '',
term_dates_url: '',
staff_list_url: '',
} : undefined,
};
}, [bootstrapData]);
useEffect(() => {
if (accessToken && !tree) fetchTree();
@@ -553,9 +564,6 @@ export function CCGraphNavPanel() {
}
}, [tree]);
useEffect(() => {
if (accessToken && !schoolStatus) fetchSchoolStatus();
}, [accessToken, schoolStatus, fetchSchoolStatus]);
// Fetch timetable term nodes when switching to term view
useEffect(() => {
@@ -637,7 +645,6 @@ export function CCGraphNavPanel() {
const refreshAll = useCallback(() => {
setTree(null);
setSchoolStatus(null);
setAcademicCalendarStatus('idle');
setAcademicTerms([]);
}, []);
@@ -726,9 +733,8 @@ export function CCGraphNavPanel() {
onClose={() => setOnboardingWizardOpen(false)}
onComplete={() => {
setOnboardingWizardOpen(false);
// Reload tree + school status after successful onboarding
// Reload tree after successful onboarding; bootstrap state refreshes from AuthContext
setTree(null);
setSchoolStatus(null);
}}
apiBase={apiBase}
/>