|
JS8Call-Improved master
|
Orchestrates per-band persistent activity storage (issue #267). More...
#include <ActivityStorageController.h>


Classes | |
| struct | Context |
| The window state and operations this controller needs. More... | |
Public Member Functions | |
| ActivityStorageController (Context context, QObject *parent=nullptr) | |
| Construct the controller over an already-built window. | |
| bool | importLegacyActivityIfNeeded () |
| Import the legacy .ini activity once per configuration. | |
| void | beginStartupGrace () |
| Start the grace period allowed for the rig to report a band. | |
| void | showLegacyRxTextIfDegraded () |
| Show the legacy .ini RX text when the store is unavailable. | |
| void | setupRxTextAutosave () |
| Connect the debounced RX-text write-back timers. | |
| QString | currentBucket () const |
| The bucket the on-screen panes belong to ("" = out-of-plan). | |
| bool | isBucketLoaded () const |
| Whether a bucket has been restored into the panes yet. | |
| void | bandUnchanged (QString const &band) |
| The rig reported the band the panes already hold. | |
| void | bandChanged (QString const &band) |
| The rig reported a different band; park and switch buckets. | |
| void | bucketRestored (QString const &band, bool paneReloaded) |
| Record that the window has restored a bucket's panes. | |
| void | persistCallActivity (CallDetail const &d, bool fallbackToCurrentBand=false) |
| Upsert one station into the store. | |
| void | adjustCallActivityOffsets (int hzDelta) |
| Shift every stored offset for the current bucket. | |
| void | beginBatch () |
| Open a write batch; nested batches share one transaction. | |
| void | endBatch () |
| Close a write batch, committing at the outermost level. | |
| void | clearAllActivity () |
| Drop every stored row, across all buckets and both panes. | |
| void | clearRxActivity () |
| Drop the current bucket's stored RX text. | |
| void | clearCallActivity () |
| Drop the current bucket's stored Call Activity rows. | |
| void | removeStoredCall (QString const &call) |
| Drop one station from the current bucket. | |
| void | flushOnClose () |
| Final flush: sweep every confirmed bucket's pending writes. | |
Orchestrates per-band persistent activity storage (issue #267).
ActivityDB is the SQLite wrapper; this is everything above it - which bucket the panes belong to, when a bucket's stored history may be merged into the session, when the RX pane may be written back, the write batching, the one-time legacy .ini import, and the close-time sweep. It deliberately does not know about UI_Constructor: the window state it needs arrives through Context, so the window keeps only a pointer to this object and thin delegating calls.
"Bucket" rather than "band" throughout: the bucket a pane belongs to is a band name, or "" - the out-of-plan bucket, which persists activity heard on dials outside the band plan (transverter IFs, channelized operation) the way the legacy un-banded ini did.
|
explicit |
Construct the controller over an already-built window.
| context | The window state and callbacks it may use. |
| parent | Optional QObject parent. UI_Constructor passes none and holds the sole owning std::unique_ptr instead: the window is the only thing that ever refers to this object, and single ownership keeps the teardown order obvious rather than relying on a member unique_ptr running before ~QObject reaps its children. |
| void ActivityStorageController::adjustCallActivityOffsets | ( | int | hzDelta | ) |
Shift every stored offset for the current bucket.
Rewrite every displayed offset after a waterfall nudge.
| hzDelta | Signed offset change in Hz. |
| hzDelta | The shift applied to the receiver's offsets. |
Only rows whose own dial belongs to the current band are written back: entries displayed here but keyed to another band (post-QSY stragglers, inbox senders carrying their message's dial) did not QSY, and dial-less RAM-only entries must not be promoted into the store by a nudge. The dial-less rows this bucket does store - manual adds - take the fallback path instead, since Bands::find(0) resolves to "".

| void ActivityStorageController::bandChanged | ( | QString const & | band | ) |
The rig reported a different band; park and switch buckets.
The rig reported a different band from the one on screen.
| band | The newly confirmed band name. |
| band | The band reported. |
The confirmed flag is set after switchActivityBucket(), so the flush inside it still treats the outgoing bucket as the startup guess it is. The band is also recorded as confirmed, so the close-time sweep does not skip its RX text.
| void ActivityStorageController::bandUnchanged | ( | QString const & | band | ) |
The rig reported the band the panes already hold.
The rig reported the band the window already believes it is on.
| band | The confirmed band name. |
| band | The band reported. |
Confirms the bucket, moving the panes first if they are still showing the startup guess. The confirmed flag is set only after switchActivityBucket(), so the flush inside that switch still sees the outgoing bucket as unconfirmed - otherwise the startup guess's pane, which by then holds this band's decodes too, is written under the guessed band.
| void ActivityStorageController::beginBatch | ( | ) |
Open a write batch; nested batches share one transaction.
Batches are lazy: opening one records only the intent and the transaction starts at the first write inside it, so a decode cycle that persists nothing costs nothing - no BEGIN IMMEDIATE taking the write lock on the GUI thread with its 5 s busy timeout, and no store-reopen probe on behalf of a caller that never writes. The outermost begin also clears the previous batch's failed-BEGIN mark, so a batch whose transaction could not be opened does not suppress the next one's.

| void ActivityStorageController::beginStartupGrace | ( | ) |
Start the grace period allowed for the rig to report a band.
Start the grace period for an as-yet unconfirmed bucket.
Until it does (or this lapses, covering Rig=None), RX text is not filed: it carries no per-line frequency, so the bucket would only be the previous session's guess.
While it runs, RX text for the startup guess is held in the pane rather than filed under a band the rig has not reported; see saveRxTextForBand().
| void ActivityStorageController::bucketRestored | ( | QString const & | band, |
| bool | paneReloaded ) |
Record that the window has restored a bucket's panes.
Take up the bucket the window has just restored on screen.
| band | The bucket restored. |
| paneReloaded | true if the RX pane was replaced wholesale. |
| band | The bucket now displayed. |
| paneReloaded | True when the panes were actually reloaded from the RAM band caches; false when re-entering the bucket already on screen, where the live panes are newer than any cache entry. |
A reload disarms the saver and resyncs its change tracking, since what was restored is what the cache holds. A band whose last flush failed is the exception: its tracking is left at the -1 sentinel so it keeps reporting unsaved, and its debounce is restarted here, because only document activity would otherwise arm it and a quiet band would never retry.
| void ActivityStorageController::clearAllActivity | ( | ) |
Drop every stored row, across all buckets and both panes.
Storage half of the "Clear All Activity" action.
Called after the window has cleared the panes: clearActivity()'s inbox refresh re-persists every unread sender and would otherwise repopulate the store the wipe had just emptied.
"All" spans every band and the session's RAM band caches, not just the bucket on screen: dropping only the current band's rows would let the others reload as if never cleared, and a hop back to a previously visited band would restore its pre-clear snapshot for the next debounced save to write straight back into the store. Every bucket is then marked unseeded - on success because the store is empty, on failure because the panes no longer hold the stored history and a save would destroy it. A session whose store is disabled empties the panes and says so, rather than reporting a delete that did not happen.
| void ActivityStorageController::clearCallActivity | ( | ) |
Drop the current bucket's stored Call Activity rows.
The "Clear Call Activity" action, store and pane.
Bucket-scoped by design: a row displayed here but stored under another band (its own dial's band - e.g. a decode that completed just after a QSY) is that band's history, which this action cannot see and must not destroy. Unread inbox senders reappear regardless - they are re-synthesized from inbox.db3, the source of truth for unread mail.
On a failed delete the bucket is deliberately not un-seeded: the RX pane is untouched and still contains this bucket's stored text, so forcing a re-seed would splice that history in a second time. The rows simply return at the next session, which the message above reports. A session whose store is disabled clears the pane and says so, rather than reporting a delete that did not happen.
| void ActivityStorageController::clearRxActivity | ( | ) |
Drop the current bucket's stored RX text.
The "Clear RX Activity" action, store and pane.
The store is cleared before the pane: if the delete fails the pane keeps its text, so no later seed can splice the stored history back in and re-persist it, silently undoing the clear. On that failure the bucket is forced unseeded before saves resume - the pane no longer holds the stored text, so a save would overwrite it - and the debounce the pane clear arms is stopped, or its retry would seed the bucket and put the "cleared" text back on screen. A session whose store is disabled clears the pane and says so, rather than reporting a delete that did not happen.
| void ActivityStorageController::endBatch | ( | ) |
Close a write batch, committing at the outermost level.
Close the outermost batch, committing its transaction.

| void ActivityStorageController::flushOnClose | ( | ) |
Final flush: sweep every confirmed bucket's pending writes.
Final flush of everything the session has not written yet.
Covers anything the debounced save timer has not written. A bucket that was never seeded - the store was unusable at its first visit - gets one last seed attempt first, because saves are suppressed while unseeded and the whole session's RX text would otherwise be discarded, where the legacy ini path always wrote it. m_activityShuttingDown is then set so that the flush cannot re-enter the seed's own retry, and its table rebuild, while the window is tearing down.
Bands whose text never reached the store - visited while it was down, or left before a seed succeeded - survive only in the RAM band cache, and this is their last chance. A seeded bucket's cached document already contains that bucket's stored history, so it replaces the stored row; an unseeded one does not, so it is appended to whatever is stored rather than replacing it.
| bool ActivityStorageController::importLegacyActivityIfNeeded | ( | ) |
Import the legacy .ini activity once per configuration.
One-time import of the legacy .ini activity data into activity.db3, following the inbox_v1 -> inbox_v2 pattern: the legacy [CallActivity] group and RXActivity key are read once and left in place for older versions of the software.
Rows are attributed to the band each record was heard on, via its stored dial; the RX text blob has no per-line frequency, so it goes to the band of the last-known dial. The fire-once marker is a row in the store keyed by the configuration id, so it follows the data it gates; one that cannot be read defers the import rather than risk a re-import.
A clone arrives with a marker naming its source in place of an id of its own; the source's rows are copied under the fresh id first, and the marker is removed only once that copy has run, so a failure retries at the next start rather than losing the inherited history. The import is one transaction with the marker inside it: marking a partial import done would lose the failed rows, the ini group never being re-read.
A requested reset that cannot run disables the session, and the legacy ini keys are purged even then: they hold a copy of the same activity, which a later reopen or un-tick would otherwise splice back.
| void ActivityStorageController::persistCallActivity | ( | CallDetail const & | d, |
| bool | fallbackToCurrentBand = false ) |
Upsert one station into the store.
Write one call-activity row to the store.
| d | The station as displayed. |
| fallbackToCurrentBand | File under the on-screen bucket when the record carries no usable dial frequency of its own. |
| d | The row to persist. |
| fallbackToCurrentBand | File a row carrying no dial under the bucket on screen; passed only where that is genuinely where it belongs (a manually added station, a logbook grid backfill, and qsy()'s offset write-back for those same rows). |
Rows are filed under the band of their own dial frequency, because processDecodeEvent() deliberately stamps records with the capture-time dial so that decodes completing after a QSY, and inbox senders, keep the band they were heard on - keying by the live band would be issue #267 all over again. A dial that resolves to no band files under "", the out-of-plan bucket.
A row filed under a bucket other than the one on screen is merged into that bucket's cached table rather than forcing a re-seed of it: the seed's RX-text merge is not idempotent against a pane restored from the RAM cache, so un-seeding a bucket to refresh its call table would splice that bucket's stored history in behind itself on every return visit. The merge follows the seed's own rules - a cached row with a strictly newer timestamp stands, otherwise the incoming row replaces it and carries the cached row's enrichment across.

| void ActivityStorageController::removeStoredCall | ( | QString const & | call | ) |
Drop one station from the current bucket.
Drop one station's stored row for the bucket on screen.
| call | The callsign to remove. |
| call | The callsign the operator removed from the table. |
Bucket-scoped: only the on-screen bucket's stored row is deleted. A row the same station holds on another band is that band's history, which this context cannot see and must not destroy; an unread inbox sender reappears regardless, re-synthesized from inbox.db3. Rows are stored under the trimmed callsign. A closed store is reported as such, rather than as the row being filed under another band; that state is read before the delete, so a delete whose own failure closes the handle still reports only the removal. That message is read off the handle directly, as endBatch() does, so a third-strike self-close cannot add the accessor's own status message on top of this one.
| void ActivityStorageController::setupRxTextAutosave | ( | ) |
Connect the debounced RX-text write-back timers.
Wire up the debounced write-on-change persistence of the RX pane.
Any change re-arms the short debounce, so the stored copy trails the pane by at most a few seconds instead of being written only at shutdown. The second timer is armed once and deliberately not restarted by further changes: sustained sub-interval traffic (fast submodes, busy nets) would otherwise re-arm the debounce faster than it can fire, deferring the write - and growing the crash-loss window - indefinitely.
| void ActivityStorageController::showLegacyRxTextIfDegraded | ( | ) |
Show the legacy .ini RX text when the store is unavailable.
Show the legacy ini RX text on a degraded or disabled start.
Display only, and recorded as such, so a later recovery does not bank a second copy of history the import already holds.
Purely for display, and only on a degraded start or a disabled session: nothing here can reach the store either way. The window's startup already restored that copy from the ini and never rewrites it, so showing it beats showing an empty pane.
The copy's extent is recorded in m_rxTextLegacyBand and m_rxTextLegacyBlocks, because the recovery seed and the close-time sweep treat only the text below it as this session's own.