mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-06 19:57:50 +00:00
feat: reorganize Maintenance settings workflows (#1329)
This commit is contained in:
parent
544771334f
commit
659e9d66c8
3 changed files with 176 additions and 151 deletions
|
|
@ -26,7 +26,7 @@ In Board Only mode, General, Board, and Tasks are marked as the primary path. Ev
|
|||
- `SettingsLocalNav` is reserved for related sections on a long page. It uses in-page anchors, remains horizontally scrollable at narrow widths, and does not promote subsections into global destinations.
|
||||
- `SettingsHelpText`, `SettingsErrorText`, `SettingsUnit`, and `SettingsActionGroup` standardize secondary explanation, validation, numeric units, routine transfer actions, and destructive actions.
|
||||
|
||||
General, Board, Tasks, and Data are the reference migrations. Data demonstrates continuous local navigation; simpler pages omit it. Agents extends that pattern with anchored Providers, Compatibility, Profiles, Health, and Policies workflows. Notifications uses Health, Buzz, Reply Adapters, Delivery, and Preferences to separate diagnostics and sensitive delivery configuration from routine choices. Multi-user uses Workspace, Members, Invitations, Devices, and API Access to keep routine collaboration separate from sensitive device and credential controls. Maintenance still requires dedicated workflow decomposition because it combines several independent operational surfaces.
|
||||
General, Board, Tasks, and Data are the reference migrations. Data demonstrates continuous local navigation; simpler pages omit it. Agents extends that pattern with anchored Providers, Compatibility, Profiles, Health, and Policies workflows. Notifications uses Health, Buzz, Reply Adapters, Delivery, and Preferences to separate diagnostics and sensitive delivery configuration from routine choices. Multi-user uses Workspace, Members, Invitations, Devices, and API Access to keep routine collaboration separate from sensitive device and credential controls. Maintenance uses Overview, Cleanup, Logs, Backup/Restore, and Lifecycle to separate routine diagnostics from destructive cleanup and advanced data movement.
|
||||
|
||||
## Focus and responsive behavior
|
||||
|
||||
|
|
|
|||
|
|
@ -185,9 +185,14 @@ describe('Maintenance settings tab', () => {
|
|||
});
|
||||
|
||||
it('renders health, storage, cleanup preview, logs, lifecycle, and backup controls', async () => {
|
||||
renderWithProviders(<MaintenanceTab />);
|
||||
const { container } = renderWithProviders(<MaintenanceTab />);
|
||||
|
||||
expect(await screen.findByText('Maintenance Center')).toBeDefined();
|
||||
expect(await screen.findByRole('heading', { name: 'Maintenance' })).toBeDefined();
|
||||
expect(screen.getByRole('navigation', { name: 'Maintenance settings sections' })).toBeDefined();
|
||||
expect(screen.getByRole('link', { name: 'Backup/Restore' }).getAttribute('href')).toBe(
|
||||
'#maintenance-backup'
|
||||
);
|
||||
expect(container.querySelectorAll('[data-settings-section]')).toHaveLength(5);
|
||||
expect(screen.getByText('Runtime storage is readable and writable.')).toBeDefined();
|
||||
expect(screen.getByText('Archived report')).toBeDefined();
|
||||
expect(
|
||||
|
|
@ -232,7 +237,7 @@ describe('Maintenance settings tab', () => {
|
|||
it('submits backup export with optional workspace scope', async () => {
|
||||
renderWithProviders(<MaintenanceTab />);
|
||||
|
||||
await screen.findByText('Maintenance Center');
|
||||
await screen.findByRole('heading', { name: 'Maintenance' });
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'SQLite path' }), {
|
||||
target: { value: '/tmp/veritas.db' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import type {
|
|||
MaintenanceHealthCheck,
|
||||
MaintenanceStorageCategory,
|
||||
} from '@veritas-kanban/shared';
|
||||
import { SettingsLocalNav, SettingsPage, SettingsSection } from '../shared';
|
||||
|
||||
const HEALTH_COLORS: Record<MaintenanceHealthCheck['state'], string> = {
|
||||
ok: 'green',
|
||||
|
|
@ -116,39 +117,44 @@ function HealthCheckList({ checks }: { checks: MaintenanceHealthCheck[] }) {
|
|||
|
||||
function StorageTable({ categories }: { categories: MaintenanceStorageCategory[] }) {
|
||||
return (
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Artifact</Table.Th>
|
||||
<Table.Th>Items</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Cleanup</Table.Th>
|
||||
<Table.Th>Last used</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{categories.map((category) => (
|
||||
<Table.Tr key={category.id}>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>
|
||||
{category.label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{category.retainedReason}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{category.itemCount}</Table.Td>
|
||||
<Table.Td>{formatBytes(category.bytes)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={category.cleanupEligibleCount > 0 ? 'yellow' : 'gray'} variant="light">
|
||||
{category.cleanupEligibleCount}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{formatDate(category.lastUsedAt)}</Table.Td>
|
||||
<Table.ScrollContainer minWidth={640}>
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Artifact</Table.Th>
|
||||
<Table.Th>Items</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Cleanup</Table.Th>
|
||||
<Table.Th>Last used</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{categories.map((category) => (
|
||||
<Table.Tr key={category.id}>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>
|
||||
{category.label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{category.retainedReason}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{category.itemCount}</Table.Td>
|
||||
<Table.Td>{formatBytes(category.bytes)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={category.cleanupEligibleCount > 0 ? 'yellow' : 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{category.cleanupEligibleCount}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{formatDate(category.lastUsedAt)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -309,17 +315,10 @@ export function MaintenanceTab() {
|
|||
: 0;
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Text size="sm" fw={700}>
|
||||
Maintenance Center
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{summary.storageMode} storage, {summary.mode} mode, refreshed{' '}
|
||||
{formatDate(summary.generatedAt)}
|
||||
</Text>
|
||||
</div>
|
||||
<SettingsPage
|
||||
title="Maintenance"
|
||||
description={`${summary.storageMode} storage, ${summary.mode} mode, refreshed ${formatDate(summary.generatedAt)}.`}
|
||||
actions={
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Refresh maintenance state">
|
||||
<Button
|
||||
|
|
@ -342,59 +341,71 @@ export function MaintenanceTab() {
|
|||
Debug Bundle
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<SettingsLocalNav
|
||||
label="Maintenance settings sections"
|
||||
items={[
|
||||
{ id: 'maintenance-overview', label: 'Overview' },
|
||||
{ id: 'maintenance-cleanup', label: 'Cleanup' },
|
||||
{ id: 'maintenance-logs', label: 'Logs' },
|
||||
{ id: 'maintenance-backup', label: 'Backup/Restore' },
|
||||
{ id: 'maintenance-lifecycle', label: 'Lifecycle' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xs">
|
||||
<SummaryMetric
|
||||
label="Storage"
|
||||
value={formatBytes(summary.storage.totalBytes)}
|
||||
icon={HardDrive}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Cleanup Preview"
|
||||
value={`${summary.cleanupPreview.items.length} items`}
|
||||
icon={Trash2}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Work Products"
|
||||
value={`${summary.workProducts.totals.products} products`}
|
||||
icon={Archive}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Lifecycle Classes"
|
||||
value={`${summary.lifecycle.length} classes`}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<SettingsSection
|
||||
id="maintenance-overview"
|
||||
title="Overview"
|
||||
description="Review service health, storage usage, and current maintenance scope."
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xs">
|
||||
<SummaryMetric
|
||||
label="Storage"
|
||||
value={formatBytes(summary.storage.totalBytes)}
|
||||
icon={HardDrive}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Cleanup Preview"
|
||||
value={`${summary.cleanupPreview.items.length} items`}
|
||||
icon={Trash2}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Work Products"
|
||||
value={`${summary.workProducts.totals.products} products`}
|
||||
icon={Archive}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="Lifecycle Classes"
|
||||
value={`${summary.lifecycle.length} classes`}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Text size="sm" fw={700}>
|
||||
Health
|
||||
</Text>
|
||||
<HealthCheckList checks={summary.health} />
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Group justify="space-between">
|
||||
<div className="mt-4 space-y-3">
|
||||
<Text size="sm" fw={700}>
|
||||
Storage Usage
|
||||
Health
|
||||
</Text>
|
||||
<Badge variant="light">{formatBytes(summary.storage.totalBytes)}</Badge>
|
||||
</Group>
|
||||
<StorageTable categories={summary.storage.categories} />
|
||||
</section>
|
||||
<HealthCheckList checks={summary.health} />
|
||||
</div>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<div className="mt-4 space-y-3">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={700}>
|
||||
Cleanup Preview
|
||||
Storage Usage
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{formatBytes(cleanupBytes)} across {summary.cleanupPreview.items.length} previewed
|
||||
items
|
||||
</Text>
|
||||
</div>
|
||||
<Badge variant="light">{formatBytes(summary.storage.totalBytes)}</Badge>
|
||||
</Group>
|
||||
<StorageTable categories={summary.storage.categories} />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="maintenance-cleanup"
|
||||
title="Cleanup"
|
||||
description={`${formatBytes(cleanupBytes)} across ${summary.cleanupPreview.items.length} previewed items. Review retained reasons before taking action.`}
|
||||
tone="danger"
|
||||
actions={
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
|
|
@ -405,59 +416,67 @@ export function MaintenanceTab() {
|
|||
>
|
||||
Review Cleanup
|
||||
</Button>
|
||||
</Group>
|
||||
<CleanupPreviewList items={summary.cleanupPreview.items} />
|
||||
<Progress
|
||||
value={workProductRatio}
|
||||
size="sm"
|
||||
color="yellow"
|
||||
aria-label="Work product cleanup ratio"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Text size="sm" fw={700}>
|
||||
Logs
|
||||
</Text>
|
||||
<Group align="flex-end" gap="sm">
|
||||
<Select
|
||||
label="Source"
|
||||
value={selectedLog}
|
||||
onChange={(value) => value && setSelectedLog(value)}
|
||||
data={logOptions}
|
||||
leftSection={<FileClock className="h-4 w-4" />}
|
||||
className="flex-1"
|
||||
}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<CleanupPreviewList items={summary.cleanupPreview.items} />
|
||||
<Progress
|
||||
value={workProductRatio}
|
||||
size="sm"
|
||||
color="yellow"
|
||||
aria-label="Work product cleanup ratio"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tail"
|
||||
value={tailLines}
|
||||
onChange={(value) => setTailLines(typeof value === 'number' ? value : 200)}
|
||||
min={1}
|
||||
max={500}
|
||||
w={120}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
leftSection={<RefreshCcw className="h-4 w-4" />}
|
||||
onClick={() => logQuery.refetch()}
|
||||
>
|
||||
Tail
|
||||
</Button>
|
||||
</Group>
|
||||
<Textarea
|
||||
aria-label="Redacted log tail"
|
||||
value={logQuery.data?.lines.join('\n') ?? ''}
|
||||
minRows={8}
|
||||
readOnly
|
||||
styles={{ input: { fontFamily: 'var(--mantine-font-family-monospace)' } }}
|
||||
/>
|
||||
</section>
|
||||
</Stack>
|
||||
</SettingsSection>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Text size="sm" fw={700}>
|
||||
Backup and Restore
|
||||
</Text>
|
||||
<SettingsSection
|
||||
id="maintenance-logs"
|
||||
title="Logs"
|
||||
description="Tail a redacted diagnostic source without exposing stored secrets."
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-3 sm:grid-cols-[minmax(0,1fr)_7rem_auto]">
|
||||
<Select
|
||||
label="Source"
|
||||
value={selectedLog}
|
||||
onChange={(value) => value && setSelectedLog(value)}
|
||||
data={logOptions}
|
||||
leftSection={<FileClock className="h-4 w-4" />}
|
||||
className="col-span-2 min-w-0 sm:col-span-1"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tail"
|
||||
value={tailLines}
|
||||
onChange={(value) => setTailLines(typeof value === 'number' ? value : 200)}
|
||||
min={1}
|
||||
max={500}
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
leftSection={<RefreshCcw className="h-4 w-4" />}
|
||||
onClick={() => logQuery.refetch()}
|
||||
>
|
||||
Tail
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label="Redacted log tail"
|
||||
value={logQuery.data?.lines.join('\n') ?? ''}
|
||||
minRows={8}
|
||||
readOnly
|
||||
styles={{ input: { fontFamily: 'var(--mantine-font-family-monospace)' } }}
|
||||
/>
|
||||
</Stack>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="maintenance-backup"
|
||||
title="Backup and Restore"
|
||||
description="Export or import SQLite data through explicit source and destination paths."
|
||||
tone="advanced"
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
|
|
@ -525,12 +544,13 @@ export function MaintenanceTab() {
|
|||
{lastBackupResult && <Code block>{lastBackupResult}</Code>}
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
</section>
|
||||
</SettingsSection>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Text size="sm" fw={700}>
|
||||
Lifecycle Policy
|
||||
</Text>
|
||||
<SettingsSection
|
||||
id="maintenance-lifecycle"
|
||||
title="Lifecycle Policy"
|
||||
description="Review retention classes and the kinds of sensitive data each class contains."
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
|
||||
{summary.lifecycle.map((entry) => (
|
||||
<Paper key={entry.id} withBorder radius="md" p="sm">
|
||||
|
|
@ -552,7 +572,7 @@ export function MaintenanceTab() {
|
|||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</section>
|
||||
</SettingsSection>
|
||||
|
||||
<Modal
|
||||
opened={cleanupOpen}
|
||||
|
|
@ -581,6 +601,6 @@ export function MaintenanceTab() {
|
|||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</SettingsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue