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 */}
+
+
+ {/* 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) => (
))}
@@ -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' }}
+ />
+