fix(wavelog): a 200 is not an acceptance

WaveLogApi decided an upload had succeeded from the HTTP status alone. Wavelog validates
after responding, so a rejected QSO comes back as 200 with `{"status":"failed","reason":
"..."}` - and the uploader then called markUploaded and dropped it from the queue. The
contact was lost and the operator was told the upload succeeded.

Response shapes are transcribed from the Wavelog API reference, not guessed: success is
`status: success` or `successful`, a duplicate is `status: dupe` with a 200, failures are
`status: failed` with `reason` or `status: error` with `message`.

WavelogResponse reads the body. Four outcomes: accepted and duplicate both clear the
queue entry, because the log holds the QSO either way; rejected keeps it and surfaces the
server's own explanation; and a status field we cannot recognise also keeps it, since
costing a retry beats losing a contact. Parsed as text rather than with JSONObject because
org.json is compileOnly in core:domain and a JVM test would otherwise assert against a
stub. Whitespace around separators is collapsed before matching - a first attempt listed
spacings and missed `{ "status" : "failed" }`, which a probe caught.

Two other things in the same area.

The ten-minute auto-upload loop is gone. It retried the queue in the background with no
way to tell the operator anything: a grid mismatch was swallowed by an empty if block and
every other failure retried silently forever. A QSO that cannot be uploaded now waits for
a manual upload from settings, where the result is actually shown.

The upload path no longer builds user-facing text in Kotlin. UploadOutcome carried a
pre-formatted Chinese string, so the message ignored the device language whatever the
locale files said. It now reports a Reason the view model maps to resources, which needed
a format-argument overload on IShowToast to get a count into a localised message.
This commit is contained in:
mckero committed 2026-08-26 01:20:26 +00:00
1 parent fe6d0af8b7
commit 4567f46867
10 files changed
+392 -39

No files matched your search

@@ -275,9 +275,11 @@ class SettingsViewModel(
viewModelScope.launch {
when (val r = lotwRepo.refresh()) {
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Ok ->
showToast("LoTW 卫星列表已更新: ${r.count} 个")
showToast(R.string.lotw_list_updated, r.count)
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Error ->
wavelogError = "LoTW 卫星列表更新失败: ${r.message}"
// Server's own words only: the dialog supplies the framing, so the wording
// stays in the resource files rather than being built here in one language.
wavelogError = r.message
}
}
}
@@ -286,21 +288,35 @@ class SettingsViewModel(
viewModelScope.launch {
val result = pendingUploader?.uploadQueue()
when (result) {
is UploadOutcome.Done -> {
if (result.failedCount > 0 && result.firstError.isNotBlank()) {
wavelogError = result.firstError
} else {
showToast("WaveLog: ${result.message}")
}
}
is UploadOutcome.Done -> reportUpload(result)
is UploadOutcome.NeedConfirm -> {
gridConfirm = GridConfirmData(result.stationGrid, result.userGrid)
}
null -> showToast("WaveLog: 上传失败")
null -> showToast(R.string.wavelog_upload_failed)
}
}
}
/**
* Report an upload outcome, keeping the wording in the resource files.
*
* The server's own explanation goes to [wavelogError] rather than a Toast, because it is the
* only thing that says why Wavelog refused a QSO and a Toast is gone in three seconds.
*/
private fun reportUpload(result: UploadOutcome.Done) {
when {
result.failedCount > 0 && result.firstError.isNotBlank() ->
wavelogError = result.firstError
result.reason == UploadOutcome.Reason.NOT_CONFIGURED ->
showToast(R.string.wavelog_not_configured)
result.reason == UploadOutcome.Reason.NO_STATION_INFO ->
showToast(R.string.wavelog_no_station)
result.failedCount > 0 ->
showToast(R.string.wavelog_upload_done, result.successCount, result.failedCount)
else -> showToast(R.string.wavelog_upload_all_ok, result.successCount)
}
}
/** Grid-confirm result: ignore and upload / cancel */
fun resolveGridConfirm(ignoreAndUpload: Boolean) {
val confirm = gridConfirm ?: return
@@ -309,8 +325,8 @@ class SettingsViewModel(
viewModelScope.launch {
val result = pendingUploader?.uploadQueue(force = true)
when (result) {
is UploadOutcome.Done -> showToast("WaveLog: ${result.message}")
else -> showToast("WaveLog: 上传失败")
is UploadOutcome.Done -> reportUpload(result)
else -> showToast(R.string.wavelog_upload_failed)
}
}
}