Split out config menus

This commit is contained in:
Jack Kingsman
2026-01-13 14:14:03 -08:00
parent 999ab37bb0
commit 55d68beeb7
8 changed files with 703 additions and 651 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

537
frontend/dist/assets/index-CUZgfnRO.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-BAMaL3S8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dtp6aYf1.css">
<script type="module" crossorigin src="/assets/index-CUZgfnRO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-49wEGwkK.css">
</head>
<body>
<div id="root"></div>

View File

@@ -8,6 +8,7 @@ import { MessageList } from './components/MessageList';
import { MessageInput, type MessageInputHandle } from './components/MessageInput';
import { NewMessageModal } from './components/NewMessageModal';
import { ConfigModal } from './components/ConfigModal';
import { MaintenanceModal } from './components/MaintenanceModal';
import { RawPacketList } from './components/RawPacketList';
import { CrackerPanel } from './components/CrackerPanel';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './components/ui/sheet';
@@ -46,6 +47,7 @@ export function App() {
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
const [showNewMessage, setShowNewMessage] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [showMaintenance, setShowMaintenance] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [undecryptedCount, setUndecryptedCount] = useState(0);
const [showCracker, setShowCracker] = useState(false);
@@ -513,6 +515,7 @@ export function App() {
health={health}
config={config}
onConfigClick={() => setShowConfig(true)}
onMaintenanceClick={() => setShowMaintenance(true)}
onAdvertise={handleAdvertise}
onMenuClick={() => setSidebarOpen(true)}
/>
@@ -662,12 +665,17 @@ export function App() {
open={showConfig}
config={config}
appSettings={appSettings}
health={health}
onClose={() => setShowConfig(false)}
onSave={handleSaveConfig}
onSaveAppSettings={handleSaveAppSettings}
onSetPrivateKey={handleSetPrivateKey}
onReboot={handleReboot}
/>
<MaintenanceModal
open={showMaintenance}
health={health}
onClose={() => setShowMaintenance(false)}
onHealthRefresh={async () => {
const data = await api.getHealth();
setHealth(data);

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import type { AppSettings, AppSettingsUpdate, HealthStatus, RadioConfig, RadioConfigUpdate } from '../types';
import type { AppSettings, AppSettingsUpdate, RadioConfig, RadioConfigUpdate } from '../types';
import {
Dialog,
DialogContent,
@@ -12,33 +12,27 @@ import { Label } from './ui/label';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Alert, AlertDescription } from './ui/alert';
import { toast } from './ui/sonner';
import { api } from '../api';
interface ConfigModalProps {
open: boolean;
config: RadioConfig | null;
appSettings: AppSettings | null;
health: HealthStatus | null;
onClose: () => void;
onSave: (update: RadioConfigUpdate) => Promise<void>;
onSaveAppSettings: (update: AppSettingsUpdate) => Promise<void>;
onSetPrivateKey: (key: string) => Promise<void>;
onReboot: () => Promise<void>;
onHealthRefresh: () => Promise<void>;
}
export function ConfigModal({
open,
config,
appSettings,
health,
onClose,
onSave,
onSaveAppSettings,
onSetPrivateKey,
onReboot,
onHealthRefresh,
}: ConfigModalProps) {
const [name, setName] = useState('');
const [lat, setLat] = useState('');
@@ -50,11 +44,8 @@ export function ConfigModal({
const [cr, setCr] = useState('');
const [privateKey, setPrivateKey] = useState('');
const [maxRadioContacts, setMaxRadioContacts] = useState('');
const [retentionDays, setRetentionDays] = useState('14');
const [loading, setLoading] = useState(false);
const [rebooting, setRebooting] = useState(false);
const [cleaning, setCleaning] = useState(false);
const [deduping, setDeduping] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
@@ -144,58 +135,6 @@ export function ConfigModal({
}
};
const handleCleanup = async () => {
const days = parseInt(retentionDays, 10);
if (isNaN(days) || days < 1) {
setError('Retention days must be at least 1');
return;
}
setError('');
setCleaning(true);
try {
const result = await api.runMaintenance(days);
toast.success('Database cleanup complete', {
description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`,
});
// Refresh health to get updated database size
await onHealthRefresh();
} catch (err) {
console.error('Failed to run maintenance:', err);
toast.error('Database cleanup failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setCleaning(false);
}
};
const handleDedup = async () => {
setError('');
setDeduping(true);
try {
const result = await api.deduplicatePackets();
if (result.started) {
toast.success('Deduplication started', {
description: result.message,
});
} else {
toast.info('Deduplication', {
description: result.message,
});
}
} catch (err) {
console.error('Failed to start deduplication:', err);
toast.error('Deduplication failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setDeduping(false);
}
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
@@ -369,52 +308,6 @@ export function ConfigModal({
</Button>
</div>
<Separator className="my-4" />
<div className="space-y-3">
<Label>Database Maintenance</Label>
<p className="text-xs text-muted-foreground">
Current database size: <span className="font-medium">{health?.database_size_mb ?? '?'} MB</span>
</p>
<p className="text-xs text-muted-foreground">
Delete undecrypted packets older than the specified days. This helps manage storage
for packets that couldn't be decrypted (unknown channel keys).
</p>
<div className="flex gap-2 items-end">
<div className="space-y-1">
<Label htmlFor="retention-days" className="text-xs">Days to retain</Label>
<Input
id="retention-days"
type="number"
min="1"
max="365"
value={retentionDays}
onChange={(e) => setRetentionDays(e.target.value)}
className="w-20"
/>
</div>
<Button
variant="outline"
onClick={handleCleanup}
disabled={cleaning || loading}
>
{cleaning ? 'Cleaning...' : 'Cleanup'}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
Remove packets with duplicate payloads (same message received via different paths).
Runs in background and may take a long time.
</p>
<Button
variant="outline"
onClick={handleDedup}
disabled={deduping || loading}
>
{deduping ? 'Starting...' : 'Remove Duplicates'}
</Button>
</div>
{error && (
<div className="text-sm text-destructive">{error}</div>
)}

View File

@@ -0,0 +1,143 @@
import { useState } from 'react';
import type { HealthStatus } from '../types';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Button } from './ui/button';
import { toast } from './ui/sonner';
import { api } from '../api';
interface MaintenanceModalProps {
open: boolean;
health: HealthStatus | null;
onClose: () => void;
onHealthRefresh: () => Promise<void>;
}
export function MaintenanceModal({
open,
health,
onClose,
onHealthRefresh,
}: MaintenanceModalProps) {
const [retentionDays, setRetentionDays] = useState('14');
const [cleaning, setCleaning] = useState(false);
const [deduping, setDeduping] = useState(false);
const handleCleanup = async () => {
const days = parseInt(retentionDays, 10);
if (isNaN(days) || days < 1) {
toast.error('Invalid retention days', {
description: 'Retention days must be at least 1',
});
return;
}
setCleaning(true);
try {
const result = await api.runMaintenance(days);
toast.success('Database cleanup complete', {
description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`,
});
// Refresh health to get updated database size
await onHealthRefresh();
} catch (err) {
console.error('Failed to run maintenance:', err);
toast.error('Database cleanup failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setCleaning(false);
}
};
const handleDedup = async () => {
setDeduping(true);
try {
const result = await api.deduplicatePackets();
if (result.started) {
toast.success('Deduplication started', {
description: result.message,
});
} else {
toast.info('Deduplication', {
description: result.message,
});
}
} catch (err) {
console.error('Failed to start deduplication:', err);
toast.error('Deduplication failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setDeduping(false);
}
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Database Maintenance</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Current database size: <span className="font-medium">{health?.database_size_mb ?? '?'} MB</span>
</p>
<div className="space-y-3">
<Label>Cleanup Old Packets</Label>
<p className="text-xs text-muted-foreground">
Delete undecrypted packets older than the specified days. This helps manage storage
for packets that couldn't be decrypted (unknown channel keys).
</p>
<div className="flex gap-2 items-end">
<div className="space-y-1">
<Label htmlFor="retention-days" className="text-xs">Days to retain</Label>
<Input
id="retention-days"
type="number"
min="1"
max="365"
value={retentionDays}
onChange={(e) => setRetentionDays(e.target.value)}
className="w-20"
/>
</div>
<Button
variant="outline"
onClick={handleCleanup}
disabled={cleaning}
>
{cleaning ? 'Cleaning...' : 'Cleanup'}
</Button>
</div>
</div>
<div className="space-y-3">
<Label>Remove Duplicate Packets</Label>
<p className="text-xs text-muted-foreground">
Remove packets with duplicate payloads (same message received via different paths).
Runs in background and may take a long time.
</p>
<Button
variant="outline"
onClick={handleDedup}
disabled={deduping}
>
{deduping ? 'Starting...' : 'Remove Duplicates'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -8,11 +8,12 @@ interface StatusBarProps {
health: HealthStatus | null;
config: RadioConfig | null;
onConfigClick: () => void;
onMaintenanceClick: () => void;
onAdvertise: () => void;
onMenuClick?: () => void;
}
export function StatusBar({ health, config, onConfigClick, onAdvertise, onMenuClick }: StatusBarProps) {
export function StatusBar({ health, config, onConfigClick, onMaintenanceClick, onAdvertise, onMenuClick }: StatusBarProps) {
const connected = health?.radio_connected ?? false;
const [reconnecting, setReconnecting] = useState(false);
@@ -88,11 +89,18 @@ export function StatusBar({ health, config, onConfigClick, onAdvertise, onMenuCl
>
Advertise
</button>
<button
onClick={onMaintenanceClick}
className="px-2 py-1 bg-[#333] border border-[#444] text-[#e0e0e0] rounded text-xs cursor-pointer hover:bg-[#444]"
title="Database Maintenance"
>
<span role="img" aria-label="Settings">&#9881;&#65039;</span>
</button>
<button
onClick={onConfigClick}
className="px-3 py-1 bg-[#333] border border-[#444] text-[#e0e0e0] rounded text-xs cursor-pointer hover:bg-[#444]"
>
Config
<span role="img" aria-label="Radio">&#128251;</span> Config
</button>
</div>
);