From 2bac6655f3ecfa4b4c8939ada1d1356fd59101c1 Mon Sep 17 00:00:00 2001 From: QIU Date: Fri, 14 Aug 2026 18:08:46 +0000 Subject: [PATCH] fix(amsat): align status slots to their UTC calendar-day labels AmSatRepository labelled columns by calendar date but filled them by slicing a rolling 72-slot window ending at fetch time. The two timelines coincide only near 23:59 UTC. At common fetch times the status grid lied about dates: UTC 00:00: 72 / 72 slots under the wrong label "today" column contained all of yesterday UTC 12:00: 36 / 72 wrong; every column straddled two dates UTC 13:37: 30 / 72 wrong UTC 23:59: 0 / 72 wrong (the accidental alignment case) Anchor the six columns on UTC midnight instead. Every SatDay now covers exactly [day 00:00, next day 00:00), split into twelve 2-hour slots newest-first so the UI's existing first-non-gray lookup still chooses the latest daily report. A standalone Java probe porting the old arithmetic reproduced the 72/72, 36/72 and 30/72 mismatches. Porting the new formula gives 0/72 mismatches at 00:00, 12:00, 13:37 and 23:59 UTC. Also restore core:data's unit-test compilation. DatabaseRepoTest's fakes were stale after IRemoteSource gained AMSAT methods and ISettingsRepo's zero-arg GPS setter became suspend; the whole data test suite previously could not compile, so data-layer regressions were untestable. Updated the fake members and verified :core:data:testDebugUnitTest plus :core:data:compileReleaseKotlin BUILD SUCCESSFUL. The product code does not use org.json in JVM tests because Android org.json stubs throw there, so the date math remains verified by the standalone same-JVM probe rather than a misleading mocked parser test. --- .../core/data/repository/AmSatRepository.kt | 44 ++++++++++++------- .../core/data/repository/DatabaseRepoTest.kt | 8 +++- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt index fe0d058a..7574d70e 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt @@ -92,28 +92,38 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor val byName = reports.groupBy { it.name } val monthAbbr = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + // Anchor the columns on UTC midnight. Slicing a rolling "N x 2 hours ago" + // window into six groups of twelve does not line up with calendar days + // unless the fetch happens just before 24:00 UTC: at 00:00 UTC every one + // of the 72 slots sat under the wrong date label, and at 12:00 UTC half of + // them did, so the column headed "today" showed yesterday's reports. + val todayStartSec = (nowSec / 86400L) * 86400L val labels = (0 until 6).map { d -> - utc.timeInMillis = (nowSec - d * 86400L) * 1000 + utc.timeInMillis = (todayStartSec - d * 86400L) * 1000 "${monthAbbr[utc.get(Calendar.MONTH)]} ${utc.get(Calendar.DAY_OF_MONTH)}" } return names.map { name -> - val slots = (0 until 72).map { slotIdx -> - val slotStart = nowSec - (slotIdx + 1) * 7200L - val slotEnd = nowSec - slotIdx * 7200L - val inSlot = byName[name].orEmpty().filter { it.reportedTimeUtcSec in slotStart until slotEnd } - if (inSlot.isEmpty()) { - SatSlot(statusColor = NO_REPORT_GRAY, count = 0) - } else { - val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!! - SatSlot( - statusColor = statusColorOf(newest.report), - count = inSlot.size, - reportIds = inSlot.map { it.id } - ) - } - } + val reportsForSat = byName[name].orEmpty() val days = (0 until 6).map { d -> - SatDay(dateLabel = labels[d], slots = slots.subList(d * 12, (d + 1) * 12)) + val dayStart = todayStartSec - d * 86400L + // Newest slot first: the UI picks the first non-gray slot of a day + // as that day's status. + val slots = (0 until 12).map { slotInDay -> + val slotStart = dayStart + (11 - slotInDay) * 7200L + val slotEnd = slotStart + 7200L + val inSlot = reportsForSat.filter { it.reportedTimeUtcSec in slotStart until slotEnd } + if (inSlot.isEmpty()) { + SatSlot(statusColor = NO_REPORT_GRAY, count = 0) + } else { + val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!! + SatSlot( + statusColor = statusColorOf(newest.report), + count = inSlot.size, + reportIds = inSlot.map { it.id } + ) + } + } + SatDay(dateLabel = labels[d], slots = slots) } SatStatus(name = name, days = days) } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt index 19710d4b..5ea8d969 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -125,6 +125,12 @@ private class FakeRemoteSource : IRemoteSource { override suspend fun getFileStream(uri: String): InputStream? = fileStreams[uri]?.invoke() override suspend fun getNetworkStream(url: String): InputStream? = networkStreams[url]?.invoke() + + override suspend fun getStatusHtml(): String? = null + + override suspend fun getAmSatCatalog(): String? = null + + override suspend fun getAmSatReports(hours: Int, limit: Int): String? = null } private class FakeLocalSource : ILocalSource { @@ -200,7 +206,7 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true - override fun setStationPosition(): Boolean = true + override suspend fun setStationPosition(): Boolean = true override fun setStationPosition(locator: String): Boolean = true