feat(wavelog): upload rides the LoTW flow — auto-sync + catch-up (option A)

Standalone Wavelog upload entries are gone: the batch is prepared alongside the LoTW preview (count shown in both preview dialogs), posted right after the LoTW confirm in the radar, settings-logbook and resubmit flows, and the backlog flushes when LoTW has nothing pending. Failures stay unmarked and retry on the next upload.
This commit is contained in:
atsunatsu committed 2026-10-05 02:19:11 +08:00
1 parent a459cfb046
commit 5a8c4281a3
11 files changed
+243 -268

No files matched your search

@@ -56,11 +56,35 @@ fun wavelogResultMessage(result: WavelogUploadOutcome): String = when (result) {
append("All ${result.markIds.size} record(s) already in Wavelog")
} else {
append("Uploaded ${result.imported} QSO(s) to Wavelog")
if (result.duplicates > 0) append(" — ${result.duplicates} already there")
}
if (result.duplicates > 0) append(" — ${result.duplicates} already there")
if (result.skipped > 0) append(" — ${result.skipped} skipped by Wavelog")
}
is WavelogUploadOutcome.Rejected -> result.message
is WavelogUploadOutcome.Failed -> result.message
WavelogUploadOutcome.Expired -> "Preview expired — tap upload again"
}
/**
* Message for the piggybacked Wavelog side when nothing can be submitted —
* null when there is nothing to say (fully caught up, no held-back records).
*/
fun wavelogIdleSegment(preview: WavelogUploadPreview): String? = when {
preview.blockedBy != null -> "Wavelog: ${preview.blockedBy.label()}"
preview.skipped > 0 -> "Wavelog: ${wavelogSkipSummary(preview)}"
else -> null
}
/**
* Message for a finished piggybacked Wavelog upload: the server outcome plus
* the records held back before the POST (e.g. grids outside the station profile).
*/
fun wavelogConfirmedSegment(outcome: WavelogUploadOutcome, preview: WavelogUploadPreview): String {
val base = wavelogResultMessage(outcome)
val head = if (outcome is WavelogUploadOutcome.Imported) base else "Wavelog: $base"
val skip = wavelogSkipSummary(preview)
return if (skip.isNotBlank()) "$head\n$skip" else head
}
/** Message when the piggybacked upload threw outright — records stay unmarked for retry. */
fun wavelogTransportFailureSegment(): String = "Wavelog: upload failed — it will retry with your next upload"
@@ -11,7 +11,10 @@
package com.rtbishop.look4sat.core.domain.logbook
import com.rtbishop.look4sat.core.domain.repository.WavelogProblem
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadOutcome
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -32,6 +35,23 @@ class WavelogUploadMessagesTest {
wavelogUploaded = wavelogUploaded
)
private fun preview(
count: Int = 0,
skipped: Int = 0,
reasons: Map<WavelogProblem, Int> = emptyMap(),
blockedBy: WavelogProblem? = null
) = WavelogUploadPreview(
count = count,
skipped = skipped,
reasons = reasons,
stationLabel = "OL62TI · BA7OPF",
firstUtc = "",
lastUtc = "",
contacts = emptyList(),
submittedIds = List(count) { it.toLong() },
blockedBy = blockedBy
)
@Test
fun freshLocalRecordIsEligible() {
assertTrue(wavelogUploadCandidates(listOf(record())).isNotEmpty())
@@ -62,4 +82,60 @@ class WavelogUploadMessagesTest {
assertEquals("no station profile selected", WavelogProblem.NO_STATION.label())
assertEquals("logged grid outside the Wavelog station grid", WavelogProblem.GRID_MISMATCH.label())
}
// --- Piggyback segments (Wavelog rides the LoTW upload flow) ---
@Test
fun `idle segment stays silent when nothing was held back`() {
assertNull(wavelogIdleSegment(preview()))
assertNull(wavelogIdleSegment(preview(count = 0, skipped = 0)))
}
@Test
fun `idle segment explains a blocked profile`() {
assertEquals(
"Wavelog: no station profile selected",
wavelogIdleSegment(preview(blockedBy = WavelogProblem.NO_STATION))
)
}
@Test
fun `idle segment lists held-back records with reasons`() {
assertEquals(
"Wavelog: 3 QSO(s) skipped — 3× logged grid outside the Wavelog station grid",
wavelogIdleSegment(preview(skipped = 3, reasons = mapOf(WavelogProblem.GRID_MISMATCH to 3)))
)
}
@Test
fun `confirmed segment reports the outcome and pre-POST skips`() {
val outcome = WavelogUploadOutcome.Imported(imported = 2, duplicates = 0, skipped = 0, markIds = listOf(1, 2))
assertEquals("Uploaded 2 QSO(s) to Wavelog", wavelogConfirmedSegment(outcome, preview()))
assertEquals(
"Uploaded 2 QSO(s) to Wavelog\n1 QSO(s) skipped — 1× logged grid outside the Wavelog station grid",
wavelogConfirmedSegment(
outcome,
preview(skipped = 1, reasons = mapOf(WavelogProblem.GRID_MISMATCH to 1))
)
)
}
@Test
fun `confirmed segment keeps the all-duplicates wording unprefixed`() {
val outcome = WavelogUploadOutcome.Imported(imported = 0, duplicates = 2, skipped = 0, markIds = listOf(1, 2))
assertEquals("All 2 record(s) already in Wavelog", wavelogConfirmedSegment(outcome, preview()))
}
@Test
fun `confirmed segment prefixes failures with Wavelog`() {
assertEquals("Wavelog: boom", wavelogConfirmedSegment(WavelogUploadOutcome.Failed("boom"), preview()))
}
@Test
fun `transport failure segment asks for a retry on the next upload`() {
assertEquals(
"Wavelog: upload failed — it will retry with your next upload",
wavelogTransportFailureSegment()
)
}
}