fix: Bug where renaming the main folder did not update db path

settings where the ones of the previous folder.
This commit is contained in:
2026-06-18 11:20:20 +02:00
parent 59f1775fcd
commit b6d991b799
6 changed files with 93 additions and 33 deletions
+23
View File
@@ -523,9 +523,23 @@ func (a *App) startup(ctx context.Context) {
// Windows reinstall. It lives OUTSIDE the DB since we must know the path
// before opening it.
if custom := readDBPointer(dataDir); custom != "" {
// Portability guard: a pointer that is merely ANOTHER folder's default DB
// location ("…/<other>/data/opslog.db") means the portable folder was
// renamed or copied — its config.json still points at the original. Ignore
// it and use THIS folder's own data (and clear the stale pointer so it
// stops happening). A genuine custom location — another drive, a different
// filename — is NOT default-style, so it's still honoured.
stale := strings.EqualFold(filepath.Base(custom), "opslog.db") &&
strings.EqualFold(filepath.Base(filepath.Dir(custom)), "data") &&
!strings.EqualFold(filepath.Clean(filepath.Dir(custom)), filepath.Clean(dataDir))
if stale {
fmt.Printf("OpsLog: ignoring stale DB pointer %q (folder moved) — using %s\n", custom, a.dbPath)
_ = writeDBPointer(dataDir, "")
} else {
a.dbPath = custom
usingDefault = false
}
}
if err := os.MkdirAll(filepath.Dir(a.dbPath), 0o755); err != nil {
a.startupErr = "cannot create db folder: " + err.Error()
fmt.Println("OpsLog:", a.startupErr)
@@ -3174,6 +3188,15 @@ func (a *App) DeleteQSO(id int64) error {
return a.qso.Delete(a.ctx, id)
}
// DeleteQSOs removes several QSOs at once (multi-row selection). Returns the
// number actually deleted.
func (a *App) DeleteQSOs(ids []int64) (int64, error) {
if a.qso == nil {
return 0, fmt.Errorf("db not initialized")
}
return a.qso.DeleteMany(a.ctx, ids)
}
// QSLBulkUpdate carries the paper-QSL fields to apply to a selection. An empty
// string leaves that field unchanged (so you can set only "received = Y + date"
// without touching the sent side).
+35 -23
View File
@@ -7,7 +7,7 @@ import {
import {
AddQSO, ListQSO, CountQSO, ListQSOFiltered, CountQSOFiltered,
OpenADIFFile, ImportADIF, SaveADIFFile, ExportADIF, ExportADIFFiltered, ExportADIFSelected,
GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO,
GetQSO, UpdateQSO, DeleteQSO, DeleteQSOs, DeleteAllQSO,
UpdateQSOsFromCty, UpdateQSOsFromQRZ, UpdateQSOsFromClublog, UploadQSOsManual, SendQSORecordingEmail,
LookupCallsign, GetStationSettings, GetListsSettings,
GetStartupStatus, CheckForUpdate,
@@ -701,8 +701,10 @@ export default function App() {
// === Modals ===
const [editingQSO, setEditingQSO] = useState<QSO | null>(null);
const [deletingQSO, setDeletingQSO] = useState<QSO | null>(null);
// QSOs queued for the delete confirm (1 or many — multi-row selection).
const [deletingIds, setDeletingIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [showSettings, setShowSettings] = useState(false);
// Re-read the "beam on map" toggle when Preferences closes (it's edited there).
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
@@ -1584,8 +1586,8 @@ export default function App() {
} catch (err: any) { setError(String(err?.message ?? err)); }
}
function onModalDelete(id: number) {
const q = editingQSO; setEditingQSO(null);
if (q) setDeletingQSO(q); else askDelete(id);
setEditingQSO(null);
setDeletingIds([id]);
}
// Bulk grid actions (right-click menu). Recompute country/zones from
@@ -1654,20 +1656,25 @@ export default function App() {
showToast(`Exported ${r.count} selected QSO${r.count > 1 ? 's' : ''}${r.path}`);
} catch (e: any) { setError(String(e?.message ?? e)); }
}
function askDelete(id: number) {
const q = qsos.find((x) => x.id === id);
if (q) setDeletingQSO(q);
function askDelete(id: number) { setDeletingIds([id]); }
// Delete the whole multi-row selection (Edit menu / Delete key).
function askDeleteSelected() {
if (selectedIds.length > 0) setDeletingIds(selectedIds);
else if (selectedId != null) setDeletingIds([selectedId]);
}
async function confirmDelete() {
if (!deletingQSO) return;
if (deletingIds.length === 0) return;
const ids = deletingIds;
try {
await DeleteQSO(deletingQSO.id);
if (selectedId === deletingQSO.id) setSelectedId(null);
setDeletingQSO(null);
if (ids.length === 1) await DeleteQSO(ids[0]);
else await DeleteQSOs(ids as any);
setDeletingIds([]);
setSelectedId(null);
setSelectedIds([]);
await refresh();
} catch (err: any) {
setError(String(err?.message ?? err));
setDeletingQSO(null);
setDeletingIds([]);
}
}
async function confirmDeleteAll() {
@@ -1878,7 +1885,7 @@ export default function App() {
]},
{ name: 'edit', label: 'Edit', items: [
{ type: 'item', label: 'Edit selected QSO…', action: 'edit.edit', shortcut: 'Enter', disabled: selectedId === null },
{ type: 'item', label: 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
{ type: 'item', label: selectedIds.length > 1 ? `Delete ${selectedIds.length} selected QSOs` : 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
{ type: 'separator' },
{ type: 'item', label: 'Preferences…', action: 'edit.prefs' },
]},
@@ -1901,7 +1908,7 @@ export default function App() {
{ name: 'help', label: 'Help', items: [
{ type: 'item', label: 'About OpsLog', action: 'help.about' },
]},
], [total, selectedId, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled]);
], [total, selectedId, selectedIds, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled]);
function handleMenu(action: string) {
switch (action) {
@@ -1911,7 +1918,7 @@ export default function App() {
case 'view.refresh': refresh(); break;
case 'view.clearfilters': setFilterCallsign(''); setActiveFilter({ conditions: [], match: 'AND' }); break;
case 'edit.edit': if (selectedId !== null) openEdit(selectedId); break;
case 'edit.delete': if (selectedId !== null) askDelete(selectedId); break;
case 'edit.delete': askDeleteSelected(); break;
case 'edit.prefs': setShowSettings(true); break;
case 'tools.qslmanager': setQslTabOpen(true); setActiveTab('qsl'); break;
case 'tools.qsldesigner': setQslDesignerOpen(true); break;
@@ -2008,14 +2015,14 @@ export default function App() {
}
if (typing) return;
if (selectedId !== null) {
if (e.key === 'Delete') { e.preventDefault(); askDelete(selectedId); return; }
if (e.key === 'Delete') { e.preventDefault(); askDeleteSelected(); return; }
if (e.key === 'Enter') { e.preventDefault(); openEdit(selectedId); return; }
}
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId, refresh]);
}, [selectedId, selectedIds, refresh]);
// ── Entry-field blocks ─────────────────────────────────────────────────
// Each field is defined once here, then composed into either the compact
@@ -3213,7 +3220,7 @@ export default function App() {
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
onExportSelected={exportSelectedADIF}
onExportFiltered={exportFilteredADIF}
onRowSelected={(id) => setSelectedId(id)}
onRowSelected={(ids) => { setSelectedIds(ids); setSelectedId(ids[0] ?? null); }}
/>
<div className="px-3 py-1.5 border-t border-border/60 text-[11px] text-muted-foreground flex items-center justify-between gap-3 bg-muted/30">
<div className="flex items-center gap-3">
@@ -3592,16 +3599,21 @@ export default function App() {
onOpenDesigner={() => setQslDesignerOpen(true)}
/>
{deletingQSO && (
{deletingIds.length > 0 && (() => {
const single = deletingIds.length === 1 ? qsos.find((x) => x.id === deletingIds[0]) : null;
return (
<ConfirmDialog
title="Delete QSO?"
message={`This will permanently delete the QSO with ${deletingQSO.callsign} on ${fmtDateUTC(deletingQSO.qso_date)} (${deletingQSO.band} ${deletingQSO.mode}). This cannot be undone.`}
title={deletingIds.length > 1 ? `Delete ${deletingIds.length} QSOs?` : 'Delete QSO?'}
message={single
? `This will permanently delete the QSO with ${single.callsign} on ${fmtDateUTC(single.qso_date)} (${single.band} ${single.mode}). This cannot be undone.`
: `This will permanently delete ${deletingIds.length} selected QSO${deletingIds.length > 1 ? 's' : ''}. This cannot be undone.`}
confirmLabel="Delete"
danger
onConfirm={confirmDelete}
onCancel={() => setDeletingQSO(null)}
onCancel={() => setDeletingIds([])}
/>
)}
);
})()}
{showDeleteAll && (
<ConfirmDialog
title="Delete ALL QSOs?"
+3 -3
View File
@@ -46,7 +46,7 @@ type Props = {
rows: QSOForm[];
total: number;
onRowDoubleClicked?: (q: QSOForm) => void;
onRowSelected?: (id: number | null) => void;
onRowSelected?: (ids: number[]) => void;
onUpdateFromCty?: (ids: number[]) => void;
onUpdateFromQRZ?: (ids: number[]) => void;
onUpdateFromClublog?: (ids: number[]) => void;
@@ -277,8 +277,8 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data);
}
function onSelectionChanged() {
const sel = gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined;
onRowSelected?.(sel && sel[0] ? (sel[0].id as number) : null);
const sel = (gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined) ?? [];
onRowSelected?.(sel.map((r) => r.id as number).filter((id) => id != null));
}
// ── Column picker (visibility) ──
+2
View File
@@ -87,6 +87,8 @@ export function DeleteProfile(arg1:number):Promise<void>;
export function DeleteQSO(arg1:number):Promise<void>;
export function DeleteQSOs(arg1:Array<number>):Promise<number>;
export function DeleteUDPIntegration(arg1:number):Promise<void>;
export function DisconnectAllClusters():Promise<void>;
+4
View File
@@ -146,6 +146,10 @@ export function DeleteQSO(arg1) {
return window['go']['main']['App']['DeleteQSO'](arg1);
}
export function DeleteQSOs(arg1) {
return window['go']['main']['App']['DeleteQSOs'](arg1);
}
export function DeleteUDPIntegration(arg1) {
return window['go']['main']['App']['DeleteUDPIntegration'](arg1);
}
+19
View File
@@ -651,6 +651,25 @@ func (r *Repo) DeleteAll(ctx context.Context) (int64, error) {
return n, nil
}
// DeleteMany removes several QSOs in one statement. Returns the number deleted.
func (r *Repo) DeleteMany(ctx context.Context, ids []int64) (int64, error) {
if len(ids) == 0 {
return 0, nil
}
ph := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
ph[i] = "?"
args[i] = id
}
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id IN (`+strings.Join(ph, ",")+`)`, args...)
if err != nil {
return 0, fmt.Errorf("delete qsos: %w", err)
}
n, _ := res.RowsAffected()
return n, nil
}
// Delete removes a QSO by id.
func (r *Repo) Delete(ctx context.Context, id int64) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id = ?`, id)