fix: Bug when opening another SQLite

This commit is contained in:
2026-07-06 09:50:23 +02:00
parent 06183bd5d4
commit 7b0a1ac832
5 changed files with 109 additions and 90 deletions
+69 -79
View File
@@ -3,7 +3,7 @@ import {
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
ChevronDown, ChevronRight,
User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power,
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check,
} from 'lucide-react';
import {
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
@@ -26,9 +26,8 @@ import {
ConnectClusterServer, DisconnectClusterServer,
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, QuitApp, CreateDatabase,
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase,
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
GetDataDir,
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
GetTelemetryEnabled, SetTelemetryEnabled,
GetLiveStatusEnabled, SetLiveStatusEnabled,
@@ -988,7 +987,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [mysqlMsg, setMysqlMsg] = useState('');
const [restartMsg, setRestartMsg] = useState(''); // backend switch / save → "restart to apply"
const [backendStatus, setBackendStatus] = useState<{ active: string; fallback: boolean; error: string } | null>(null);
const [dataDir, setDataDir] = useState('');
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
@@ -1081,7 +1079,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
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();
setStationLocations((locs ?? []).map((l: any) => l.name).filter(Boolean));
@@ -3506,13 +3503,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
function DatabasePanel() {
async function refreshDb() { try { setDbSettings(await GetDatabaseSettings() as any); } catch {} }
async function refreshBackend() { try { setBackendStatus(await GetDBBackendStatus() as any); } catch {} }
// The chosen file is persisted (config.json) the moment it's picked; dbMsg
// holds the pending path so the banner can offer a one-click restart to load
// it (the DB pointer is only read at startup).
async function openExisting() {
try {
const p = await PickOpenDatabase();
if (!p) return;
await OpenDatabase(p);
await refreshDb();
setDbMsg(`Database set to:\n${p}\nRestart OpsLog to apply.`);
setDbMsg(p);
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function saveCopy() {
@@ -3521,7 +3522,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
if (!p) return;
await MoveDatabase(p);
await refreshDb();
setDbMsg(`A copy was saved to:\n${p}\nand selected. Restart OpsLog to apply.`);
setDbMsg(p);
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function createNew() {
@@ -3530,29 +3531,45 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
if (!p) return;
await CreateDatabase(p);
await refreshDb();
setDbMsg(`New empty logbook created at:\n${p}\nand selected. Restart OpsLog to apply.`);
setDbMsg(p);
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function resetDefault() {
try {
await ResetDatabaseToDefault();
await refreshDb();
setDbMsg('Database reset to the default location. Restart OpsLog to apply.');
setDbMsg(dbSettings.default_path || '');
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
// Switching the logbook backend applies immediately (no restart): the local
// SQLite file always stays the config store; only the QSO logbook moves.
function useLocalLogbook() {
SaveMySQLSettings({ ...mysqlCfg, enabled: false } as any)
.then(async () => { setMysqlField({ enabled: false }); setRestartMsg(t('db.switchedSqlite')); await refreshBackend(); })
.catch((e: any) => setErr(String(e?.message ?? e)));
}
function connectMysql() {
SaveMySQLSettings(mysqlCfg as any)
.then(async () => { setRestartMsg(t('db.switchedMysql')); await refreshBackend(); })
.catch((e: any) => setErr(String(e?.message ?? e)));
}
return (
<>
<SectionHeader
title={t('sec.database')}
/>
{/* Backend selector for the ACTIVE PROFILE's logbook. Each profile can
target its own database; choosing here and Save switches the live
logbook immediately (no restart). */}
<SectionHeader title={t('sec.database')} />
{/* Logbook backend: local SQLite file (solo) or shared MySQL (multi-op).
Switching is instant — no restart. Only changing the local SQLite
FILE needs a restart (it also stores this operator's settings). */}
<div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-1">
<Label className="text-sm">Backend</Label>
<Label className="text-sm">{t('db.backend')}</Label>
<Select
value={mysqlCfg.enabled ? 'mysql' : 'sqlite'}
onValueChange={(v) => setMysqlField({ enabled: v === 'mysql' })}
onValueChange={(v) => {
const enabled = v === 'mysql';
setMysqlField({ enabled });
setRestartMsg('');
if (!enabled) useLocalLogbook(); // switching to local applies at once
}}
>
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
<SelectContent>
@@ -3561,50 +3578,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</SelectContent>
</Select>
</div>
<p className="text-[11px] text-muted-foreground max-w-2xl mb-3">
{t('db.profileHint')}
</p>
<p className="text-[11px] text-muted-foreground max-w-2xl mb-3">{t('db.profileHint')}</p>
{/* Save (always visible) applies the active profile's DB target live. */}
<div className="max-w-2xl mb-4 flex items-center gap-3">
<Button size="sm" className="h-8"
onClick={() => {
SaveMySQLSettings(mysqlCfg as any)
.then(() => setRestartMsg(mysqlCfg.enabled
? t('db.switchedMysql')
: t('db.switchedSqlite')))
.catch((e: any) => setErr(String(e?.message ?? e)));
}}>
{t('db.saveSwitch')}
</Button>
{restartMsg && <span className="text-[11px] text-success">{restartMsg}</span>}
</div>
{/* Active-backend status: confirms what OpsLog actually opened at launch. */}
{/* Compact active-backend confirmation / MySQL-fallback warning. */}
{backendStatus && (
<div className="max-w-2xl mb-4">
{backendStatus.fallback ? (
<div className="text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
{t('db.fallback')}
<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>{t('db.logbook')}</strong> {t('db.mysqlConnected')}
<span className="mx-1.5 text-muted-foreground">·</span>
<strong>{t('db.config')}</strong> {t('db.localSqlite')}
<div className="text-[10px] text-muted-foreground mt-1">
{t('db.mysqlNote')}
</div>
</div>
) : (
<div className="text-xs bg-muted/40 border border-border rounded-md px-3 py-2">
{t('db.activeBackend')} <strong className="uppercase">{backendStatus.active}</strong>
</div>
)}
</div>
backendStatus.fallback ? (
<div className="max-w-2xl mb-4 text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
{t('db.fallback')}
<div className="font-mono text-[10px] mt-1 break-all">{backendStatus.error}</div>
</div>
) : (
<div className="max-w-2xl mb-4 text-[11px] text-muted-foreground">
{t('db.activeBackend')} <strong className="uppercase text-foreground">{backendStatus.active}</strong>
{backendStatus.active === 'mysql' && <span> · {t('db.configLocal')}</span>}
</div>
)
)}
{/* SQLite: local logbook file management */}
{!mysqlCfg.enabled && (
<div className="space-y-4 max-w-2xl">
<div className="space-y-1">
@@ -3620,26 +3611,35 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={createNew}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
<Button variant="outline" size="sm" onClick={openExisting}>{t('db.openExisting')}</Button>
<Button variant="outline" size="sm" onClick={saveCopy}>{t('db.saveCopy')}</Button>
<Button variant="outline" size="sm" onClick={openExisting}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
<Button variant="outline" size="sm" onClick={saveCopy}><Copy className="size-3.5" /> {t('db.saveCopy')}</Button>
{dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>{t('db.resetDefault')}</Button>}
</div>
{dbMsg && (
<div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-2 flex items-center justify-between gap-3 whitespace-pre-line">
<span>{dbMsg}</span>
<Button size="sm" variant="destructive" className="shrink-0" onClick={() => QuitApp()}>{t('db.quitNow')}</Button>
<div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-3 space-y-2">
<div className="flex items-start gap-2">
<Check className="size-4 mt-0.5 shrink-0" />
<div>
<div className="font-medium">{t('db.savedRestart')}</div>
<div className="font-mono text-[10px] mt-1 break-all opacity-90">{dbMsg}</div>
</div>
</div>
<div className="flex items-center gap-2 pl-6">
<Button size="sm" onClick={() => { RestartApp().catch((e: any) => setErr(String(e?.message ?? e))); }}>
<Power className="size-3.5" /> {t('db.restartNow')}
</Button>
<span className="text-[10px] opacity-80">{t('db.restartHint')}</span>
</div>
</div>
)}
</div>
)}
{/* Shared MySQL database (multi-operator) */}
{/* MySQL: shared logbook connection (multi-operator) */}
{mysqlCfg.enabled && (
<div className="space-y-3 max-w-2xl">
<div className="text-[11px] text-muted-foreground leading-relaxed">
{t('db.mysqlHint')}
</div>
<div className="text-[11px] text-muted-foreground leading-relaxed">{t('db.mysqlHint')}</div>
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
<Label className="text-sm">{t('db.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 })} />
@@ -3652,28 +3652,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<Label className="text-sm">{t('es.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">
<div className="flex items-center gap-3 flex-wrap">
<Button variant="outline" size="sm" className="h-8"
onClick={() => { setMysqlMsg(t('db.testing')); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg(t('db.connectedReady'))).catch((e: any) => setMysqlMsg(t('db.failed') + String(e?.message ?? e))); }}>
{t('db.testCreate')}
</Button>
<Button size="sm" className="h-8" onClick={connectMysql}>{t('db.connectUse')}</Button>
<span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
</div>
</div>
)}
{/* Data location */}
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
<div>
<div className="text-sm font-medium">{t('db.dataLocation')}</div>
</div>
<div className="space-y-1">
<Label>{t('db.currentDataDir')}</Label>
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
{dataDir || '—'}
</div>
</div>
</div>
{restartMsg && <div className="max-w-2xl mt-3 text-[11px] text-success">{restartMsg}</div>}
{/* Backup settings, merged into this Database section. */}
<div className="border-t border-border/60 mt-6 pt-5">
+4
View File
@@ -149,6 +149,8 @@ const en: Dict = {
// Database panel
'db.optSqlite': 'SQLite — local file (solo)', 'db.optMysql': 'MySQL — shared server (multi-operator)', 'db.profileHint': 'This is the logbook for the active profile. Different profiles can point at different databases — switching profile switches the logbook.',
'db.saveSwitch': 'Save & switch logbook', 'db.switchedMysql': 'Logbook switched to MySQL ✓', 'db.switchedSqlite': 'Logbook switched to local SQLite ✓',
'db.backend': 'Backend', 'db.configLocal': 'settings stay in the local SQLite file', 'db.connectUse': 'Connect & use',
'db.savedRestart': 'Saved. Restart OpsLog to open this logbook:', 'db.restartNow': 'Restart OpsLog', 'db.restartHint': '(reopens automatically)',
'db.current': 'Current database', 'db.customLoc': '(custom location)', 'db.default': '(default)', 'db.defaultLabel': 'Default:',
'db.newDb': 'New database…', 'db.openExisting': 'Open existing…', 'db.saveCopy': 'Save a copy & switch…', 'db.resetDefault': 'Reset to default', 'db.quitNow': 'Quit now',
'db.host': 'Host', 'db.port': 'Port', 'db.database': 'Database', 'db.user': 'User', 'db.testCreate': 'Test & create database', 'db.testing': 'Testing…', 'db.connectedReady': 'Connected — database ready ✓', 'db.failed': 'Failed: ',
@@ -334,6 +336,8 @@ const fr: Dict = {
'prof.configId': 'ID de configuration', 'prof.description': 'Description', 'prof.new': 'Nouveau', 'prof.newTitle': 'Créer un nouveau profil vierge', 'prof.dupTitle': 'Cloner le profil sélectionné (garde tous ses champs)', 'prof.setActive': 'Activer', 'prof.setActiveTitle': 'Activer le profil sélectionné — les nouveaux QSO utiliseront ses champs MY_*', 'prof.deleteTitle': 'Supprimer le profil sélectionné', 'prof.cantDeleteLast': 'Impossible de supprimer le dernier profil', 'prof.activeSuffix': ' (actif)', 'prof.viewingNote': 'Tu consultes {name}. Le profil actif est {active} — ses valeurs sont inscrites sur les nouveaux QSO. Clique « Activer » pour basculer.',
'db.optSqlite': 'SQLite — fichier local (solo)', 'db.optMysql': 'MySQL — serveur partagé (multi-opérateur)', 'db.profileHint': 'Ceci est le journal du profil actif. Des profils différents peuvent pointer vers des bases différentes — changer de profil change le journal.',
'db.saveSwitch': 'Enregistrer & basculer le journal', 'db.switchedMysql': 'Journal basculé vers MySQL ✓', 'db.switchedSqlite': 'Journal basculé vers SQLite local ✓',
'db.backend': 'Base de données', 'db.configLocal': 'les réglages restent dans le fichier SQLite local', 'db.connectUse': 'Connecter et utiliser',
'db.savedRestart': 'Enregistré. Redémarrez OpsLog pour ouvrir cette base :', 'db.restartNow': 'Redémarrer OpsLog', 'db.restartHint': '(réouverture automatique)',
'db.current': 'Base actuelle', 'db.customLoc': '(emplacement personnalisé)', 'db.default': '(par défaut)', 'db.defaultLabel': 'Par défaut :',
'db.newDb': 'Nouvelle base…', 'db.openExisting': 'Ouvrir existante…', 'db.saveCopy': 'Enregistrer une copie & basculer…', 'db.resetDefault': 'Réinitialiser par défaut', 'db.quitNow': 'Quitter maintenant',
'db.host': 'Hôte', 'db.port': 'Port', 'db.database': 'Base', 'db.user': 'Utilisateur', 'db.testCreate': 'Tester & créer la base', 'db.testing': 'Test…', 'db.connectedReady': 'Connecté — base prête ✓', 'db.failed': 'Échec : ',