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
+87 -1
View File
@@ -1,8 +1,10 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
Alert,
Box,
Button,
Chip,
Container,
Grid,
Paper,
@@ -10,17 +12,28 @@ import {
Typography
} from '@mui/material';
import { useAuth } from '../../contexts/AuthContext';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import WarningIcon from '@mui/icons-material/Warning';
import { useUser } from '../../contexts/UserContext';
const DashboardPage: React.FC = () => {
const navigate = useNavigate();
const { user: authUser } = useAuth();
const { user: authUser, bootstrapData } = useAuth();
const { profile, loading } = useUser();
const displayName = profile?.display_name || authUser?.display_name || authUser?.username || 'Member';
const emailAddress = profile?.email || authUser?.email || '';
const userType = profile?.user_type || authUser?.user_type || '';
const activeInstitute = bootstrapData?.active_institute;
const activeMembership = bootstrapData?.memberships?.find(
(m) => m.institute_id === activeInstitute?.id && m.is_active
);
const schoolName = activeMembership?.institute?.name;
const onboarding = bootstrapData?.onboarding;
const calendarOk = bootstrapData?.calendar_status?.available && !bootstrapData?.calendar_status?.needs_setup;
const timetableOk = bootstrapData?.timetable_status?.available && !bootstrapData?.timetable_status?.needs_setup;
return (
<Container maxWidth="lg" sx={{ py: 6 }}>
<Stack spacing={6}>
@@ -88,6 +101,79 @@ const DashboardPage: React.FC = () => {
</Paper>
</Grid>
</Grid>
{/* Bootstrap Status Section */}
{bootstrapData && (
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Your school
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Paper elevation={2} sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Institute
</Typography>
<Stack spacing={1}>
<Typography variant="body1" fontWeight={600}>
{schoolName || 'No school assigned'}
</Typography>
{activeInstitute?.membership_role && (
<Chip
label={activeInstitute.membership_role.replace(/_/g, ' ')}
size="small"
color="primary"
variant="outlined"
/>
)}
<Typography variant="body2" color="text.secondary">
Status: {bootstrapData.school_status}
</Typography>
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={2} sx={{ p: 3, height: '100%' }}>
<Typography variant="h6" gutterBottom>
System status
</Typography>
<Stack spacing={1.5}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{calendarOk ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
<Typography variant="body2">
Calendar: {calendarOk ? 'Ready' : 'Needs setup'}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{timetableOk ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
<Typography variant="body2">
Timetable: {timetableOk ? 'Ready' : 'Needs setup'}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{bootstrapData?.graph_status?.available ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
<Typography variant="body2">
Graph projection: {bootstrapData?.graph_status?.projection_state || 'unknown'}
</Typography>
</Box>
</Stack>
</Paper>
</Grid>
</Grid>
{onboarding && onboarding.next_step !== 'complete' && (
<Alert severity="info" sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600}>
{onboarding.message}
</Typography>
<Typography variant="body2" sx={{ mt: 0.5 }}>
Next step: {onboarding.next_step.replace(/_/g, ' ')}
</Typography>
</Alert>
)}
</Stack>
)}
</Stack>
</Container>
);