up
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, QuitApp, CreateDatabase,
|
||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection,
|
||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
||||
GetDataDir,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload,
|
||||
@@ -194,11 +194,11 @@ const TREE: TreeNode[] = [
|
||||
},
|
||||
{
|
||||
kind: 'group', label: 'Hardware Configuration', icon: Server, defaultOpen: true, children: [
|
||||
{ kind: 'item', label: 'CAT interface (OmniRig)', id: 'cat' },
|
||||
{ kind: 'item', label: 'Rotator (PstRotator)', id: 'rotator' },
|
||||
{ kind: 'item', label: 'CW Keyer (WinKeyer)', id: 'winkeyer' },
|
||||
{ kind: 'item', label: 'Antenna (Ultrabeam)', id: 'antenna' },
|
||||
{ kind: 'item', label: 'Audio devices & voice keyer', id: 'audio' },
|
||||
{ kind: 'item', label: 'CAT interface', id: 'cat' },
|
||||
{ kind: 'item', label: 'Rotator', id: 'rotator' },
|
||||
{ kind: 'item', label: 'CW Keyer', id: 'winkeyer' },
|
||||
{ kind: 'item', label: 'Antenna', id: 'antenna' },
|
||||
{ kind: 'item', label: 'Audio devices', id: 'audio' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -220,7 +220,7 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
awards: 'Awards',
|
||||
cat: 'CAT interface',
|
||||
rotator: 'Rotator',
|
||||
winkeyer: 'CW Keyer (WinKeyer)',
|
||||
winkeyer: 'CW Keyer',
|
||||
antenna: 'Antenna',
|
||||
audio: 'Audio devices',
|
||||
};
|
||||
@@ -543,6 +543,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
const [mysqlCfg, setMysqlCfg] = useState<MySQLCfg>({ enabled: false, host: '', port: 3306, user: '', password: '', database: '' });
|
||||
const setMysqlField = (patch: Partial<MySQLCfg>) => setMysqlCfg((s) => ({ ...s, ...patch }));
|
||||
const [mysqlMsg, setMysqlMsg] = useState('');
|
||||
const [backendStatus, setBackendStatus] = useState<{ active: string; fallback: boolean; error: string } | null>(null);
|
||||
const [dataDir, setDataDir] = useState('');
|
||||
|
||||
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
|
||||
@@ -633,6 +634,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
setExtSvc(es as any);
|
||||
try { setDbSettings(await GetDatabaseSettings() as any); } catch {}
|
||||
try { setMysqlCfg(await GetMySQLSettings() as any); } catch {}
|
||||
try { setBackendStatus(await GetDBBackendStatus() as any); } catch {}
|
||||
try { setDataDir(await GetDataDir()); } catch {}
|
||||
try {
|
||||
const locs: any = await ListTQSLStationLocations();
|
||||
@@ -1725,7 +1727,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="CW Keyer (WinKeyer)"
|
||||
title="CW Keyer"
|
||||
hint="Drive a K1EL WinKeyer (WK1/2/3) over a serial port to send Morse from OpsLog. Enable it, pick the COM port and keying parameters, then connect from the keyer panel (Tools → WinKeyer CW keyer)."
|
||||
/>
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
@@ -2695,6 +2697,44 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<SectionHeader
|
||||
title="Database"
|
||||
/>
|
||||
{/* Backend selector — top of the panel. SQLite (solo) vs MySQL (shared). */}
|
||||
<div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-5">
|
||||
<Label className="text-sm">Backend</Label>
|
||||
<Select value={mysqlCfg.enabled ? 'mysql' : 'sqlite'} onValueChange={(v) => setMysqlField({ enabled: v === 'mysql' })}>
|
||||
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sqlite">SQLite — local file (solo)</SelectItem>
|
||||
<SelectItem value="mysql">MySQL — shared server (multi-operator)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Active-backend status: confirms what OpsLog actually opened at launch. */}
|
||||
{backendStatus && (
|
||||
<div className="max-w-2xl mb-4">
|
||||
{backendStatus.fallback ? (
|
||||
<div className="text-xs bg-amber-50 border border-amber-300 text-amber-800 rounded-md px-3 py-2">
|
||||
MySQL is enabled but the connection failed at startup — OpsLog is running on the local <strong>SQLite</strong> database.
|
||||
<div className="font-mono text-[10px] mt-1 break-all">{backendStatus.error}</div>
|
||||
</div>
|
||||
) : backendStatus.active === 'mysql' ? (
|
||||
<div className="text-xs bg-muted/40 border border-border rounded-md px-3 py-2">
|
||||
<strong>Logbook:</strong> shared MySQL <span className="text-emerald-700">connected ✓</span>
|
||||
<span className="mx-1.5 text-muted-foreground">·</span>
|
||||
<strong>Config:</strong> local SQLite
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
Only QSOs go to MySQL; your settings, profiles, rigs and cluster stay local (and fast). Existing local QSOs aren't copied — import them into the shared log if you want your history there.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs bg-muted/40 border border-border rounded-md px-3 py-2">
|
||||
Active backend: <strong className="uppercase">{backendStatus.active}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mysqlCfg.enabled && (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="space-y-1">
|
||||
<Label>Current database</Label>
|
||||
@@ -2721,50 +2761,45 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shared MySQL database (multi-operator) */}
|
||||
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Shared database (multi-operator)</div>
|
||||
{mysqlCfg.enabled && (
|
||||
<div className="space-y-3 max-w-2xl">
|
||||
<div className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
Several OpsLog instances pointed at one MySQL server see each other's QSOs live (à la Log4OM). Test the connection, then <strong>Save</strong> — OpsLog switches to MySQL (and creates all tables) on the next launch.
|
||||
</div>
|
||||
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
|
||||
<Label className="text-sm">Backend</Label>
|
||||
<Select value={mysqlCfg.enabled ? 'mysql' : 'sqlite'} onValueChange={(v) => setMysqlField({ enabled: v === 'mysql' })}>
|
||||
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sqlite">SQLite — local file (solo)</SelectItem>
|
||||
<SelectItem value="mysql">MySQL — shared server (multi-operator)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="text-sm">Host</Label>
|
||||
<Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
|
||||
<Label className="text-sm">Port</Label>
|
||||
<Input type="number" className="h-8 w-28 font-mono" value={mysqlCfg.port} onChange={(e) => setMysqlField({ port: parseInt(e.target.value, 10) || 0 })} />
|
||||
<Label className="text-sm">Database</Label>
|
||||
<Input className="h-8" placeholder="opslog" value={mysqlCfg.database} onChange={(e) => setMysqlField({ database: e.target.value })} />
|
||||
<Label className="text-sm">User</Label>
|
||||
<Input className="h-8" value={mysqlCfg.user} onChange={(e) => setMysqlField({ user: e.target.value })} />
|
||||
<Label className="text-sm">Password</Label>
|
||||
<Input type="password" className="h-8" value={mysqlCfg.password} onChange={(e) => setMysqlField({ password: e.target.value })} />
|
||||
</div>
|
||||
{mysqlCfg.enabled && (
|
||||
<>
|
||||
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
|
||||
<Label className="text-sm">Host</Label>
|
||||
<Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
|
||||
<Label className="text-sm">Port</Label>
|
||||
<Input type="number" className="h-8 w-28 font-mono" value={mysqlCfg.port} onChange={(e) => setMysqlField({ port: parseInt(e.target.value, 10) || 0 })} />
|
||||
<Label className="text-sm">Database</Label>
|
||||
<Input className="h-8" placeholder="opslog" value={mysqlCfg.database} onChange={(e) => setMysqlField({ database: e.target.value })} />
|
||||
<Label className="text-sm">User</Label>
|
||||
<Input className="h-8" value={mysqlCfg.user} onChange={(e) => setMysqlField({ user: e.target.value })} />
|
||||
<Label className="text-sm">Password</Label>
|
||||
<Input type="password" className="h-8" value={mysqlCfg.password} onChange={(e) => setMysqlField({ password: e.target.value })} />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" className="h-8"
|
||||
onClick={() => { setMysqlMsg('Testing…'); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg('Connected — database ready ✓')).catch((e: any) => setMysqlMsg('Failed: ' + String(e?.message ?? e))); }}>
|
||||
Test & create database
|
||||
</Button>
|
||||
<Button size="sm" className="h-8"
|
||||
onClick={() => { SaveMySQLSettings(mysqlCfg as any).then(() => setMysqlMsg('Saved.')).catch((e: any) => setErr(String(e?.message ?? e))); }}>
|
||||
Save
|
||||
</Button>
|
||||
<span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" className="h-8"
|
||||
onClick={() => { setMysqlMsg('Testing…'); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg('Connected — database ready ✓')).catch((e: any) => setMysqlMsg('Failed: ' + String(e?.message ?? e))); }}>
|
||||
Test & create database
|
||||
</Button>
|
||||
<Button size="sm" className="h-8"
|
||||
onClick={() => { SaveMySQLSettings(mysqlCfg as any).then(() => setMysqlMsg('Saved — restart OpsLog to connect to MySQL.')).catch((e: any) => setErr(String(e?.message ?? e))); }}>
|
||||
Save
|
||||
</Button>
|
||||
<span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
|
||||
</div>
|
||||
{mysqlMsg.startsWith('Saved') && (
|
||||
<div className="text-xs bg-emerald-50 border border-emerald-300 text-emerald-800 rounded-md px-3 py-2 flex items-center justify-between gap-3">
|
||||
<span>Saved. OpsLog will use the shared MySQL database after a restart.</span>
|
||||
<Button size="sm" variant="destructive" className="shrink-0" onClick={() => QuitApp()}>Quit now</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data location */}
|
||||
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
|
||||
|
||||
Reference in New Issue
Block a user