diff --git a/client/index.html b/client/index.html index e4b78ea..14ed24a 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Voider - VOID Management Console
diff --git a/client/src/api/authority.ts b/client/src/api/authority.ts index 1de0c0e..c34a023 100644 --- a/client/src/api/authority.ts +++ b/client/src/api/authority.ts @@ -6,6 +6,7 @@ import type { ShoulderPattern, AllocationRequest, AllocationResponse, + AllocationUpdateRequest, PaginatedAllocations, AuthorityStatistics, } from './types'; @@ -66,6 +67,24 @@ export async function getAllocation(shoulder: string): Promise { + const response = await fetch(`${getBaseUrl()}/api/authority/allocations/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || 'Failed to update allocation'); + } + return response.json(); +} + export async function getAuthorityStatistics(): Promise { const response = await fetch(`${getBaseUrl()}/api/authority/statistics`); if (!response.ok) { diff --git a/client/src/api/types.ts b/client/src/api/types.ts index bc1e121..86a0ad9 100644 --- a/client/src/api/types.ts +++ b/client/src/api/types.ts @@ -19,6 +19,7 @@ export interface AllocationRequest { contact_email: string; purpose: string; pattern_id: number; + external: boolean; } export interface AllocationResponse { @@ -30,6 +31,7 @@ export interface AllocationResponse { contact_name: string; contact_email: string; purpose: string; + external: boolean; requested_at: string; } @@ -41,9 +43,16 @@ export interface AllocationSummary { cycle_length: number; contact_name: string; contact_email: string; + purpose: string; + external: boolean; + has_minter: boolean; requested_at: string; } +export interface AllocationUpdateRequest { + external: boolean; +} + export interface PaginatedAllocations { items: AllocationSummary[]; total: number; @@ -169,5 +178,6 @@ export interface MinterStatistics { total_minted: number; total_remaining: number; utilization_percent: number; + minted_today: number; minters_by_priority: MinterResponse[]; } diff --git a/client/src/components/Layout/Layout.tsx b/client/src/components/Layout/Layout.tsx index 8a3b6da..eaa6829 100644 --- a/client/src/components/Layout/Layout.tsx +++ b/client/src/components/Layout/Layout.tsx @@ -32,7 +32,7 @@ interface NavItem { const navItems: NavItem[] = [ { text: 'Dashboard', icon: , path: '/' }, { text: 'Request Block', icon: , path: '/request' }, - { text: 'Allocations', icon: , path: '/allocations' }, + { text: 'Block Allocations', icon: , path: '/allocations' }, { text: 'Mint IDs', icon: , path: '/mint' }, { text: 'Statistics', icon: , path: '/statistics' }, ]; diff --git a/client/src/index.css b/client/src/index.css index 2c3fac6..acccf27 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -26,7 +26,7 @@ a:hover { body { margin: 0; display: flex; - place-items: center; + place-items: start; min-width: 320px; min-height: 100vh; } diff --git a/client/src/pages/Allocations.tsx b/client/src/pages/Allocations.tsx index 2f4f282..1a59021 100644 --- a/client/src/pages/Allocations.tsx +++ b/client/src/pages/Allocations.tsx @@ -15,10 +15,23 @@ import { Chip, IconButton, Tooltip, + Switch, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + FormControl, + InputLabel, + Select, + MenuItem, + Snackbar, } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { authorityApi } from '../api'; -import type { AllocationSummary, PaginatedAllocations } from '../api/types'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import { authorityApi, minterApi } from '../api'; +import type { AllocationSummary, PaginatedAllocations, GeneratorType } from '../api/types'; export default function Allocations() { const [allocations, setAllocations] = useState(null); @@ -28,20 +41,34 @@ export default function Allocations() { const [rowsPerPage, setRowsPerPage] = useState(20); const [copiedShoulder, setCopiedShoulder] = useState(null); - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const data = await authorityApi.getAllocations(page + 1, rowsPerPage); - setAllocations(data); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load allocations'); - } finally { - setLoading(false); - } - }; + // Init dialog state + const [initDialogOpen, setInitDialogOpen] = useState(false); + const [selectedAllocation, setSelectedAllocation] = useState(null); + const [generatorType, setGeneratorType] = useState('lcg'); + const [initializing, setInitializing] = useState(false); + + // Feedback state + const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ + open: false, + message: '', + severity: 'success', + }); + const [togglingExternal, setTogglingExternal] = useState(null); + const fetchData = async () => { + try { + setLoading(true); + const data = await authorityApi.getAllocations(page + 1, rowsPerPage); + setAllocations(data); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load allocations'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, [page, rowsPerPage]); @@ -74,6 +101,63 @@ export default function Allocations() { }); }; + const handleToggleExternal = async (allocation: AllocationSummary) => { + setTogglingExternal(allocation.id); + try { + await authorityApi.updateAllocation(allocation.id, { external: !allocation.external }); + setSnackbar({ + open: true, + message: `Allocation marked as ${!allocation.external ? 'external' : 'internal'}`, + severity: 'success', + }); + await fetchData(); + } catch (err) { + setSnackbar({ + open: true, + message: err instanceof Error ? err.message : 'Failed to update allocation', + severity: 'error', + }); + } finally { + setTogglingExternal(null); + } + }; + + const handleOpenInitDialog = (allocation: AllocationSummary) => { + setSelectedAllocation(allocation); + setGeneratorType('lcg'); + setInitDialogOpen(true); + }; + + const handleInitializeMinter = async () => { + if (!selectedAllocation) return; + + setInitializing(true); + try { + await minterApi.initializeMinter({ + naan: selectedAllocation.naan, + shoulder: selectedAllocation.shoulder, + blade_pattern: selectedAllocation.blade_pattern, + cycle_length: selectedAllocation.cycle_length, + generator_type: generatorType, + }); + setSnackbar({ + open: true, + message: `Minter initialized for ${selectedAllocation.shoulder}`, + severity: 'success', + }); + setInitDialogOpen(false); + await fetchData(); + } catch (err) { + setSnackbar({ + open: true, + message: err instanceof Error ? err.message : 'Failed to initialize minter', + severity: 'error', + }); + } finally { + setInitializing(false); + } + }; + if (loading && !allocations) { return ( @@ -89,7 +173,7 @@ export default function Allocations() { return ( - Shoulder Allocations + Block Allocations @@ -97,9 +181,13 @@ export default function Allocations() { - Shoulder + Block NAAN + Capacity Contact + Purpose + External + Actions Email Requested @@ -110,14 +198,14 @@ export default function Allocations() { handleCopy(allocation.shoulder)} + onClick={() => handleCopy(`${allocation.shoulder}.${allocation.blade_pattern}`)} > @@ -127,14 +215,55 @@ export default function Allocations() { + {allocation.cycle_length.toLocaleString()} {allocation.contact_name} + + + + {allocation.purpose} + + + + + handleToggleExternal(allocation)} + /> + + + {allocation.has_minter ? ( + + ) : allocation.external ? ( + + ) : ( + + handleOpenInitDialog(allocation)} + > + + + + )} + {allocation.contact_email} {formatDate(allocation.requested_at)} ))} {allocations?.items.length === 0 && ( - + No allocations yet. Request a block to get started. @@ -154,6 +283,65 @@ export default function Allocations() { onRowsPerPageChange={handleChangeRowsPerPage} /> + + {/* Initialize Minter Dialog */} + setInitDialogOpen(false)}> + Initialize Minter + + {selectedAllocation && ( + <> + + Initialize a minter for shoulder {selectedAllocation.shoulder} with{' '} + {selectedAllocation.cycle_length.toLocaleString()} identifiers. + + + NAAN: {selectedAllocation.naan} + + + Blade Pattern: {selectedAllocation.blade_pattern} + + + Generator Type + + + + )} + + + + + + + + {/* Snackbar for feedback */} + setSnackbar((prev) => ({ ...prev, open: false }))} + > + setSnackbar((prev) => ({ ...prev, open: false }))} + severity={snackbar.severity} + sx={{ width: '100%' }} + > + {snackbar.message} + + ); } diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index b26e52f..d3ed14b 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -61,10 +61,29 @@ export default function Dashboard() { + {/* Quick Info */} + + + + System Information + + + NAAN: 99999 (test) + + + Total ID Space: {formatNumber(authorityStats?.total_id_space || 0)} identifiers + + + Note: Identifiers are printed without NAAN prefix on labels. + NAAN is required for check digit validation. + + + + {/* Authority Section */} - Shoulder Allocation + Block Allocation @@ -170,32 +189,26 @@ export default function Dashboard() { - Minting Utilization + IDs Minted Today - {formatPercent(minterStats?.utilization_percent || 0)} + {formatNumber(minterStats?.minted_today || 0)} - {/* Quick Info */} - - - - System Information - - - NAAN: 99999 (test) - - - Total ID Space: {formatNumber(authorityStats?.total_id_space || 0)} identifiers - - - Note: Identifiers are printed without NAAN prefix on labels. - NAAN is required for check digit validation. - - + + + + + Minting Utilization + + + {formatPercent(minterStats?.utilization_percent || 0)} + + + diff --git a/client/src/pages/RequestBlock.tsx b/client/src/pages/RequestBlock.tsx index 8e35428..c49dc3f 100644 --- a/client/src/pages/RequestBlock.tsx +++ b/client/src/pages/RequestBlock.tsx @@ -6,6 +6,8 @@ import { TextField, Button, FormControl, + FormControlLabel, + Checkbox, InputLabel, Select, MenuItem, @@ -30,6 +32,7 @@ export default function RequestBlock() { contact_email: '', purpose: '', pattern_id: 0, + external: false, }); const [formErrors, setFormErrors] = useState>>({}); @@ -39,6 +42,7 @@ export default function RequestBlock() { try { setLoading(true); const data = await authorityApi.getPatterns(); + data.sort((a, b) => a.blade_capacity - b.blade_capacity || a.remaining_shoulders - b.remaining_shoulders); setPatterns(data); if (data.length > 0) { setFormData((prev) => ({ ...prev, pattern_id: data[0].id })); @@ -65,8 +69,8 @@ export default function RequestBlock() { errors.contact_email = 'Please enter a valid email address'; } - if (!formData.purpose.trim() || formData.purpose.length < 20) { - errors.purpose = 'Purpose must be at least 20 characters'; + if (!formData.purpose.trim()) { + errors.purpose = 'Purpose is required'; } if (!formData.pattern_id) { @@ -92,6 +96,7 @@ export default function RequestBlock() { contact_email: '', purpose: '', pattern_id: patterns[0]?.id || 0, + external: false, }); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to request allocation'); @@ -189,7 +194,7 @@ export default function RequestBlock() { > {patterns.map((pattern) => ( - {pattern.blade_capacity.toLocaleString()} IDs per shoulder ({pattern.shoulder_pattern}/{pattern.blade_pattern}) + {pattern.blade_capacity.toLocaleString()} IDs per shoulder ({pattern.shoulder_pattern}.{pattern.blade_pattern}) ))} @@ -203,11 +208,22 @@ export default function RequestBlock() { value={formData.purpose} onChange={handleChange('purpose')} error={!!formErrors.purpose} - helperText={formErrors.purpose || 'Describe how you plan to use these identifiers (min 20 characters)'} + helperText={formErrors.purpose || 'Describe how you plan to use these identifiers'} margin="normal" required /> + setFormData((prev) => ({ ...prev, external: e.target.checked }))} + /> + } + label="External use (identifiers managed outside this system)" + sx={{ mt: 1, display: 'block' }} + /> +