feat(phase-b): school/timetable wizards, graph nav panel, UI updates

New components:
- CCGraphNavPanel: Supabase-driven navigation tree (school/timetable/calendar/classes sections),
  role-aware setup buttons, lazy child loading, academic/generic calendar toggle
- SchoolCalendarWizard: 3-step admin-only school setup (details → term dates → daily periods)
- TeacherTimetableWizard: period grid with existing slot pre-loading, edit-mode title

Updated:
- CCNodeSnapshotPanel: saves via Supabase storage path + accessToken
- BasePanel: nav panel tab wired to CCGraphNavPanel
- CCFilesPanelEnhanced: auth context fixes
- CCDocumentIntelligence suite: accessToken threading, Supabase storage integration

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-26 01:25:29 +01:00
co-authored by Claude Sonnet 4.6
parent b0c7758135
commit 83adcce951
11 changed files with 1196 additions and 36 deletions
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, CircularProgress, MenuItem, Select, Typography } from '@mui/material';
import { SelectChangeEvent } from '@mui/material/Select';
import { supabase } from '../../../supabaseClient';
import { useAuth } from '../../../contexts/AuthContext';
type Manifest = {
bucket: string;
@@ -41,6 +41,7 @@ export const CCBundleViewer: React.FC<{
currentPage?: number;
combinedBundles?: Array<{ id: string }>;
}> = ({ fileId, bundleId, currentPage, combinedBundles }) => {
const { accessToken } = useAuth();
const [manifest, setManifest] = useState<Manifest | null>(null);
const [combinedManifests, setCombinedManifests] = useState<Manifest[] | null>(null);
const [mode, setMode] = useState<Mode>('markdown_full');
@@ -53,9 +54,9 @@ export const CCBundleViewer: React.FC<{
const API_BASE_FALLBACK = 'http://127.0.0.1:8080';
const proxyUrl = useCallback(async (bucket: string, relPath: string) => {
const token = (await supabase.auth.getSession()).data.session?.access_token || '';
const token = accessToken || '';
return `${API_BASE}/database/files/proxy_signed?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(relPath)}&token=${encodeURIComponent(token)}`;
}, [API_BASE]);
}, [API_BASE, accessToken]);
const replaceAllSafe = useCallback((s: string, search: string, replacement: string) => {
if (!s || typeof s !== 'string') return s || '';
@@ -222,7 +223,7 @@ export const CCBundleViewer: React.FC<{
setManifest(null);
if (combinedBundles && combinedBundles.length > 0) {
try {
const token = (await supabase.auth.getSession()).data.session?.access_token || '';
const token = accessToken || '';
const ms: Manifest[] = [];
for (const b of combinedBundles) {
const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/artefacts/${encodeURIComponent(b.id)}/manifest`, { headers: { Authorization: `Bearer ${token}` } });
@@ -239,7 +240,7 @@ export const CCBundleViewer: React.FC<{
}
if (!bundleId) return;
try {
const token = (await supabase.auth.getSession()).data.session?.access_token || '';
const token = accessToken || '';
const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/artefacts/${encodeURIComponent(bundleId)}/manifest`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(await res.text());
const rawManifest: Manifest = await res.json();
@@ -266,7 +267,7 @@ export const CCBundleViewer: React.FC<{
let textParts: string[] = [];
let jsonParts: string[] = [];
for (const m of combinedManifests) {
const token = (await supabase.auth.getSession()).data.session?.access_token || '';
const token = accessToken || '';
let rel: string | undefined;
if (mode === 'markdown_full') rel = m.markdown_full || m.html_full || m.text_full || m.json_full;
else if (mode === 'html_full') rel = m.html_full || m.markdown_full || m.text_full || m.json_full;
@@ -348,7 +349,7 @@ export const CCBundleViewer: React.FC<{
relPath = rec?.path;
}
if (!relPath) { setContent(''); setLoading(false); return; }
const token = (await supabase.auth.getSession()).data.session?.access_token || '';
const token = accessToken || '';
const url = await proxyUrl(bucket, relPath);
let res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(await res.text());