diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWUploadRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWUploadRepository.kt index e0f9e19d..a87df608 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWUploadRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWUploadRepository.kt @@ -196,6 +196,7 @@ class LoTWUploadRepository internal constructor( val ledger = ledger() var skipped = 0 var unknown = 0 + var unavailable = 0 val unique = hashSetOf() val contacts = records.sortedBy { it.startUtcMillis }.mapNotNull { record -> coroutineContext.ensureActive() @@ -205,7 +206,7 @@ class LoTWUploadRepository internal constructor( } catch (_: LoTWOperationException) { // One un-signable record must not abort the whole batch: // skip it, count it, and let the rest upload. - skipped++ + unavailable++ return@mapNotNull null } val previous = ledger[contact.fingerprint] @@ -221,7 +222,9 @@ class LoTWUploadRepository internal constructor( contacts.firstOrNull()?.record?.let { utc(it.startUtcMillis, "yyyy-MM-dd HH:mm:ss") }.orEmpty(), contacts.lastOrNull()?.record?.let { utc(it.startUtcMillis, "yyyy-MM-dd HH:mm:ss") }.orEmpty(), contacts.map { "${utc(it.record.startUtcMillis, "MM-dd HH:mm")} ${it.record.theirCallsign} ${it.fields["MODE"]} ${it.fields["SAT_NAME"].orEmpty()}" }, - unknown + unknown, + unavailable, + contacts.map { it.record.id } ) if (contacts.isNotEmpty()) pending = Pending( preview, diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ILoTWUploadRepository.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ILoTWUploadRepository.kt index 24aaef77..e851d5b0 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ILoTWUploadRepository.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ILoTWUploadRepository.kt @@ -66,7 +66,12 @@ data class LoTWUploadPreview( val firstUtc: String, val lastUtc: String, val contacts: List, - val unknownSkipped: Int = 0 + val unknownSkipped: Int = 0, + /** Un-signable records (invalid call/date/…) skipped instead of aborting the batch. */ + val unavailableSkipped: Int = 0, + /** Ids of the records that actually made it into this TQ8 batch. Only these + * may be marked "uploaded" after an accepted POST — never the full candidate list. */ + val submittedIds: List = emptyList() ) /** Local comparison against downloaded LoTW receipt flags and this app's durable upload receipts. */ diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogPage.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogPage.kt index 6f43adb7..bb9b16bf 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogPage.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogPage.kt @@ -294,6 +294,14 @@ private fun UploadPreviewDialog( }.parse(it).time }.getOrDefault(System.currentTimeMillis()) })) }Z", fontSize = 13.sp) + if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) { + val parts = buildList { + if (preview.skipped > 0) add("${preview.skipped} already uploaded/duplicate") + if (preview.unknownSkipped > 0) add("${preview.unknownSkipped} unknown result") + if (preview.unavailableSkipped > 0) add("${preview.unavailableSkipped} un-uploadable") + } + Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error) + } Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8) } }, diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogViewModel.kt index e4ee9442..1c5fbb85 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogViewModel.kt @@ -160,11 +160,18 @@ class LogViewModel( val pending = all.filter { !it.lotwConfirmed && it.status == QsoStatus.COMPLETE } val audit = lotwUploadRepository.audit(pending) if (audit.pending == 0) { - _uiState.update { it.copy(busy = false, message = "No pending QSOs to upload") } + val msg = when { + audit.unavailable > 0 -> "${audit.unavailable} QSO(s) can't be uploaded (invalid call/date — check the logbook)" + audit.unknown > 0 -> "${audit.unknown} QSO(s) had an unknown upload result — not retried automatically" + else -> "No pending QSOs to upload" + } + _uiState.update { it.copy(busy = false, message = msg) } return@launch } val preview = lotwUploadRepository.prepare(pending, false) - lastUploadedIds = pending.map { it.id } + // Only the records that actually made it into the TQ8 may be + // marked uploaded later — never the whole candidate list. + lastUploadedIds = preview.submittedIds _uiState.update { it.copy(busy = false, preview = preview) } } catch (e: LoTWOperationException) { _uiState.update { it.copy(busy = false, message = "Upload unavailable: ${e.reason}") } diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt index be13c7c9..7af71fb7 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt @@ -145,6 +145,14 @@ private fun LogbookUploadPreviewDialog( "${preview.count} QSO(s) · ${preview.firstUtc} – ${preview.lastUtc}", fontSize = 13.sp ) + if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) { + val parts = buildList { + if (preview.skipped > 0) add("${preview.skipped} already uploaded/duplicate") + if (preview.unknownSkipped > 0) add("${preview.unknownSkipped} unknown result") + if (preview.unavailableSkipped > 0) add("${preview.unavailableSkipped} un-uploadable") + } + Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error) + } Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8) } }, diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index c7f2123d..09b54652 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -367,11 +367,18 @@ class SettingsViewModel( val pending = all.filter { !it.lotwConfirmed && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE } val audit = lotwUploadRepository.audit(pending) if (audit.pending == 0) { - _uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "No pending QSOs to upload") } + val msg = when { + audit.unavailable > 0 -> "${audit.unavailable} QSO(s) can't be uploaded (invalid call/date — check the logbook)" + audit.unknown > 0 -> "${audit.unknown} QSO(s) had an unknown upload result — not retried automatically" + else -> "No pending QSOs to upload" + } + _uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = msg) } return@launch } val preview = lotwUploadRepository.prepare(pending, false) - lastLogbookUploadIds = pending.map { it.id } + // Only the records that actually made it into the TQ8 may be + // marked uploaded later — never the whole candidate list. + lastLogbookUploadIds = preview.submittedIds _uiState.update { it.copy(logbookUploadBusy = false, logbookPreview = preview) } } catch (e: com.rtbishop.look4sat.core.domain.repository.LoTWOperationException) { _uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload unavailable: ${e.reason}") }