feat: per-profile separate SQLite logbook file (config never swapped)

Design flaw: the SQLite backend conflated the app/config database (opslog.db —
settings + profiles) with the QSO logbook. connectLogbook returned a.db for any
non-MySQL profile, so the ONLY way to get a separate SQLite logbook was the
whole-app "change database location" pointer — which swapped config + profiles
too, wiping the operator's setup (reported: creating Jerem.db lost all configs).

profile.ProfileDB gains a Path field: a non-empty path routes that profile's
logbook to its own SQLite file (db.Open creates + migrates it on first use),
while opslog.db keeps settings/profiles. connectLogbook opens the file; empty
path = shared db (unchanged default, backward compatible). MySQLSettings carries
sqlite_path; Get/Save wire it through.

UI: the Database panel gains a third backend option "SQLite — separate file"
with a file picker, and now clearly labels the shared application database
(settings + profiles) vs this profile's logbook, so the two are never confused.
This commit is contained in:
2026-07-24 17:12:31 +02:00
parent bf4fba484a
commit 638ffcb326
6 changed files with 80 additions and 11 deletions
+42 -9
View File
@@ -1299,7 +1299,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [dbSettings, setDbSettings] = useState<{ path: string; default_path: string; is_custom: boolean }>({ path: '', default_path: '', is_custom: false });
const [dbMsg, setDbMsg] = useState('');
type MySQLCfg = { enabled: boolean; host: string; port: number; user: string; password: string; database: string };
type MySQLCfg = { enabled: boolean; host: string; port: number; user: string; password: string; database: string; sqlite_path?: string };
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('');
@@ -4310,10 +4310,22 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// 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(); })
SaveMySQLSettings({ ...mysqlCfg, enabled: false, sqlite_path: '' } as any)
.then(async () => { setMysqlField({ enabled: false, sqlite_path: '' }); setRestartMsg(t('db.switchedSqlite')); await refreshBackend(); })
.catch((e: any) => setErr(String(e?.message ?? e)));
}
// Route THIS profile's logbook to its own SQLite file (config stays in
// opslog.db). db.Open creates+migrates the file if it's new. Pick a name/place.
async function pickSeparateSqlite() {
try {
const p = await PickSaveDatabase();
if (!p) return;
await SaveMySQLSettings({ ...mysqlCfg, enabled: false, sqlite_path: p } as any);
setMysqlField({ enabled: false, sqlite_path: p });
setRestartMsg(t('db.switchedSqliteFile'));
await refreshBackend();
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
function connectMysql() {
SaveMySQLSettings(mysqlCfg as any)
.then(async () => { setRestartMsg(t('db.switchedMysql')); await refreshBackend(); })
@@ -4329,17 +4341,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-1">
<Label className="text-sm">{t('db.backend')}</Label>
<Select
value={mysqlCfg.enabled ? 'mysql' : 'sqlite'}
value={mysqlCfg.enabled ? 'mysql' : (mysqlCfg.sqlite_path ? 'sqlite_file' : 'sqlite')}
onValueChange={(v) => {
const enabled = v === 'mysql';
setMysqlField({ enabled });
setRestartMsg('');
if (!enabled) useLocalLogbook(); // switching to local applies at once
if (v === 'mysql') { setMysqlField({ enabled: true }); return; }
if (v === 'sqlite') { useLocalLogbook(); return; } // shared app db
// sqlite_file: keep any existing path; if none, prompt to pick one.
setMysqlField({ enabled: false });
if (!mysqlCfg.sqlite_path) pickSeparateSqlite();
}}
>
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="sqlite">{t('db.optSqlite')}</SelectItem>
<SelectItem value="sqlite_file">{t('db.optSqliteFile')}</SelectItem>
<SelectItem value="mysql">{t('db.optMysql')}</SelectItem>
</SelectContent>
</Select>
@@ -4362,10 +4377,27 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
)}
{/* SQLite: local logbook file management */}
{!mysqlCfg.enabled && (
{/* Separate per-profile SQLite logbook FILE (config stays in opslog.db) */}
{!mysqlCfg.enabled && mysqlCfg.sqlite_path && (
<div className="space-y-3 max-w-2xl">
<div className="space-y-1">
<Label>{t('db.logbookFile')}</Label>
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">{mysqlCfg.sqlite_path}</div>
<p className="text-[11px] text-muted-foreground">{t('db.logbookFileHint')}</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={pickSeparateSqlite}><FolderOpen className="size-3.5" /> {t('db.chooseFile')}</Button>
</div>
{restartMsg && <div className="text-xs text-success-muted-foreground">{restartMsg}</div>}
</div>
)}
{/* Shared application/config database (opslog.db) = settings + profiles +
this profile's QSOs when it uses the default logbook. */}
{!mysqlCfg.enabled && !mysqlCfg.sqlite_path && (
<div className="space-y-4 max-w-2xl">
<div className="space-y-1">
<Label>{t('db.current')}</Label>
<Label>{t('db.appDb')}</Label>
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
{dbSettings.path || '—'}
{dbSettings.is_custom
@@ -4373,6 +4405,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
: <span className="ml-2 text-[10px] text-muted-foreground">{t('db.default')}</span>}
</div>
<div className="text-[10px] text-muted-foreground">{t('db.defaultLabel')} <span className="font-mono">{dbSettings.default_path}</span></div>
<p className="text-[11px] text-muted-foreground">{t('db.appDbHint')}</p>
</div>
<div className="flex flex-wrap gap-2">