feat(sync): the folder loop, the hooks and the panel — it can be switched on now
The three layers underneath were already there and unreachable: the change-log format (7d664bd), the identity column (724e68b) and the no-backfill decision (c48294b). This is the wiring that gives the operator a switch. Settings, all profile-scoped, because each profile can point at its own logbook: the folder, this PC's name, the machine id minted once from it, the per-peer read offsets and the tie-break counter. Scoping the machine id is what keeps two profiles sharing one folder from writing two logbooks into one file. Three hooks. Add and update publish asynchronously, down with the rest of the after-the-fact work — a folder on a network share can block for seconds and a contact belongs on screen long before another machine hears about it. Deletion publishes SYNCHRONOUSLY and BEFORE the row goes, for the same reason deleteRemoteCopies does: once it is gone its identity is gone with it and the tombstone names nothing. The apply path uses the repository directly and never AddQSO/UpdateQSO/DeleteQSO — those publish, and a change applied here would be written straight back out, two machines echoing each other for ever. Saving writes a probe file to the chosen folder rather than asking whether it exists. A read-only cloud folder, or a share whose credentials expired, exists perfectly well and would swallow every contact in silence; if the probe fails the switch goes back off instead of sitting on while nothing is written. The panel is mostly status, and deliberately: every part of this runs on another machine and on a sync client OpsLog cannot see, so "it is not working" has to be answerable from the settings page — which PCs are in the folder, when each last logged, what is waiting unread. Four tests on the apply path, the middle two being the ones that matter: an edit made on the other PC lands on the copy already here, matched on the contact itself, instead of becoming a second row — that is what makes the no-backfill decision safe — and a contact with the same station on another band stays a separate contact.
This commit is contained in:
@@ -67,6 +67,7 @@ import (
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/spe"
|
||||
"hamlog/internal/steppir"
|
||||
"hamlog/internal/syncfolder"
|
||||
"hamlog/internal/tunergenius"
|
||||
"hamlog/internal/uls"
|
||||
"hamlog/internal/ultrabeam"
|
||||
@@ -735,6 +736,11 @@ type App struct {
|
||||
confDLCancel context.CancelFunc
|
||||
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||
syncMu sync.Mutex // serialises folder synchronisation: config, the seq counter, and the append to our own file
|
||||
syncSent int64 // changes written to the folder this session
|
||||
syncReceived int64 // changes taken from the other machines this session
|
||||
syncLast time.Time // last completed pass, for the status panel
|
||||
syncErr string // last folder error, shown in settings — a share that dropped is otherwise invisible
|
||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
||||
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
||||
@@ -1164,6 +1170,7 @@ func (a *App) startup(ctx context.Context) {
|
||||
a.backfillAwardRefsOnce() // one-time: materialise award_refs for pre-existing QSOs
|
||||
go a.rebuildWorkedIndex() // in-memory worked-index for per-spot alert checks
|
||||
go a.adifMonitorLoop() // watch external ADIF files (fldigi, N1MM…) for new QSOs
|
||||
go a.folderSyncLoop() // one operator, several PCs: read the other machines' change logs
|
||||
a.relayAutoOn.Store(a.GetRelayAuto().Enabled) // prime the relay auto-control hot-path flag
|
||||
|
||||
// cty.dat for offline DXCC / country resolution. Cached on disk; first
|
||||
@@ -2787,6 +2794,14 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
a.maybeAutoSendEQSL(qc)
|
||||
a.maybeSelfSpot(qc)
|
||||
a.publishSoon() // refresh the published web page, debounced
|
||||
// Tell the operator's other PCs. Down here with the rest of the
|
||||
// after-the-fact work because a folder on a network share can block
|
||||
// for seconds, and a contact belongs in the database and on screen
|
||||
// long before another machine needs to hear about it.
|
||||
//
|
||||
// Read back rather than sent from `qc`: award_refs was materialised
|
||||
// a few lines above and is not on the copy taken at insert time.
|
||||
a.syncPublishAsync(syncfolder.OpAdd, id, nil)
|
||||
if a.udp != nil {
|
||||
rec := adif.SingleRecordADIF(qc)
|
||||
a.udp.EmitLoggedADIF(rec)
|
||||
@@ -6014,6 +6029,7 @@ func (a *App) UpdateQSO(q qso.QSO) error {
|
||||
if err == nil {
|
||||
a.invalidateAwardStats()
|
||||
a.materializeAwardRefs(q) // fields may have changed → refresh award_refs
|
||||
a.syncPublishAsync(syncfolder.OpUpdate, q.ID, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -6107,6 +6123,7 @@ func (a *App) DeleteQSO(id int64) error {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
a.deleteRemoteCopies([]int64{id})
|
||||
a.syncPublishDeletes([]int64{id})
|
||||
return a.qso.Delete(a.ctx, id)
|
||||
}
|
||||
|
||||
@@ -6177,6 +6194,7 @@ func (a *App) DeleteQSOs(ids []int64) (int64, error) {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
a.deleteRemoteCopies(ids)
|
||||
a.syncPublishDeletes(ids)
|
||||
return a.qso.DeleteMany(a.ctx, ids)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user