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 98504e38..768cad88 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
@@ -125,6 +125,11 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor
utc.timeInMillis = (todayMidnightSec - d * 86400L) * 1000
"${monthAbbr[utc.get(Calendar.MONTH)]} ${utc.get(Calendar.DAY_OF_MONTH)}"
}
+ // Oldest report across the whole response, marking how far back the data reaches.
+ // Taken globally rather than per satellite: a quiet satellite has no reports of its
+ // own, but the slots it shares with the rest of the response were still covered.
+ val dataFromSec = reports.minOfOrNull { it.reportedTimeUtcSec } ?: todayMidnightSec
+
return names.map { name ->
val satReports = byName[name].orEmpty()
val days = (0 until 3).map { dayIdx ->
@@ -135,7 +140,9 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor
val slotEnd = slotStart + 7200L
val inSlot = satReports.filter { it.reportedTimeUtcSec in slotStart until slotEnd }
if (inSlot.isEmpty()) {
- SatSlot(statusColor = NO_REPORT_GRAY, count = 0)
+ // A slot entirely before the data starts is unknown, not silent.
+ val colour = if (slotEnd <= dataFromSec) NO_DATA_GRAY else NO_REPORT_GRAY
+ SatSlot(statusColor = colour, count = 0)
} else {
val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!!
SatSlot(
@@ -184,5 +191,15 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor
private const val NOT_HEARD_PINK = 0xFFDC267F
private const val CONFLICT_DEEP_ORANGE = 0xFFFE6100
private const val NO_REPORT_GRAY = 0xFFC0C0C0
+
+ /**
+ * Slots older than the data we actually received.
+ *
+ * The API caps at 500 records however many hours are requested. Measured live: a
+ * 72-hour request returned 500 reports spanning only 49 hours, leaving the oldest
+ * 9.5 hours of the third day with no data at all. Painting those the same grey as
+ * "nobody reported" claimed knowledge we do not have, so they get a lighter shade.
+ */
+ private const val NO_DATA_GRAY = 0xFFE8E8E8
}
}
diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt
index be354baf..6971f864 100644
--- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt
+++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt
@@ -256,4 +256,77 @@ class AmSatSlotBuildTest {
fun `an empty catalog yields no rows rather than a malformed grid`() {
assertTrue(build(emptyList(), emptyList()).isEmpty())
}
+
+ /**
+ * Slots older than the data we received must not claim nobody was listening.
+ *
+ * The API caps at 500 records however many hours are asked for. Measured live, a
+ * 72-hour request returned 500 reports covering only 49 hours, so the oldest 9.5 hours
+ * of the third day had no data at all - 352 of 3168 cells were painting "nobody heard
+ * it" over "we never looked".
+ */
+ @Test
+ fun `slots before the data starts are marked no-data, not no-report`() {
+ // The only report is midday yesterday, so nothing older than that was covered.
+ val oldestReport = utc(2026, 8, 21, 12)
+ val status = build(
+ listOf("AO-91"),
+ listOf(report("AO-91", "heard", oldestReport, id = "only"))
+ ).single()
+
+ val noReport = 0xFFC0C0C0
+ val noData = 0xFFE8E8E8
+
+ // The day before yesterday is entirely before the data begins.
+ assertTrue(
+ "every slot older than the data must read as no-data",
+ status.days[2].slots.all { it.statusColor == noData }
+ )
+
+ // Yesterday straddles it: bands after midday are covered, bands before are not.
+ val yesterday = status.days[1]
+ assertEquals("the report's own band", 1, yesterday.slots[5].count)
+ assertTrue(
+ "bands after the oldest report are covered, so silence there is real",
+ yesterday.slots.take(6).all { it.statusColor != noData }
+ )
+ assertTrue(
+ "the earliest band of yesterday is before any data",
+ yesterday.slots[11].statusColor == noData
+ )
+
+ // Today is entirely after the data starts, so its silence is genuine.
+ assertTrue(
+ "today's empty slots mean nobody reported",
+ status.days[0].slots.all { it.statusColor == noReport }
+ )
+ }
+
+ @Test
+ fun `coverage is judged from all reports, not one satellite's`() {
+ // A satellite nobody reported must not show as no-data for the whole grid: the
+ // slots were covered, that satellite simply was not heard.
+ val statuses = build(
+ listOf("LOUD", "QUIET"),
+ listOf(report("LOUD", "heard", utc(2026, 8, 20, 1), id = "early"))
+ )
+ val quiet = statuses.first { it.name == "QUIET" }
+ val noData = 0xFFE8E8E8
+
+ assertTrue(
+ "coverage reaches back to the earliest report of any satellite",
+ quiet.days.all { day -> day.slots.none { it.statusColor == noData } }
+ )
+ }
+
+ @Test
+ fun `an empty response marks nothing as covered`() {
+ // With no reports at all there is no evidence about any slot.
+ val status = build(listOf("AO-91"), emptyList()).single()
+ val noData = 0xFFE8E8E8
+ assertTrue(
+ "yesterday and earlier cannot be claimed as silent",
+ status.days.drop(1).all { day -> day.slots.all { it.statusColor == noData } }
+ )
+ }
}
diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml
index 63b77b52..d99318d5 100644
--- a/core/presentation/src/main/res/values-es/strings.xml
+++ b/core/presentation/src/main/res/values-es/strings.xml
@@ -171,4 +171,6 @@
Desplazar tonos CW fuera de rango
DeepCW solo analiza 400-1200 Hz. Si está activo, un tono fuera de ese rango se traslada al rango antes de decodificar; un tono que ya está dentro no se modifica.
+ Sin informes
+ Sin datos
diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml
index 935f221f..33f25cec 100644
--- a/core/presentation/src/main/res/values-id/strings.xml
+++ b/core/presentation/src/main/res/values-id/strings.xml
@@ -300,4 +300,6 @@
Aplikasi ini hadir tanpa jaminan
Geser nada CW di luar rentang
DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah.
+ Tidak ada laporan
+ Tidak ada data
diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml
index ee57779a..d790855e 100644
--- a/core/presentation/src/main/res/values-in/strings.xml
+++ b/core/presentation/src/main/res/values-in/strings.xml
@@ -300,4 +300,6 @@
Aplikasi ini hadir tanpa jaminan
Geser nada CW di luar rentang
DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah.
+ Tidak ada laporan
+ Tidak ada data
diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml
index 6bc0f3dc..0a166b52 100644
--- a/core/presentation/src/main/res/values-ru/strings.xml
+++ b/core/presentation/src/main/res/values-ru/strings.xml
@@ -171,4 +171,6 @@
Сдвигать CW-тоны вне диапазона
DeepCW анализирует только 400-1200 Гц. Если включено, тон вне этого диапазона переносится внутрь перед декодированием; тон внутри диапазона не изменяется.
+ Нет отчётов
+ Нет данных
diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml
index 66d0ad1a..05167d8d 100644
--- a/core/presentation/src/main/res/values-si/strings.xml
+++ b/core/presentation/src/main/res/values-si/strings.xml
@@ -171,4 +171,6 @@
පරාසයෙන් පිටත CW ස්වර මාරු කරන්න
DeepCW විශ්ලේෂණය කරන්නේ 400-1200 Hz පමණි. සක්රීය විට, එම පරාසයෙන් පිටත ස්වරයක් විකේතනයට පෙර පරාසය තුළට ගෙන එයි; දැනටමත් පරාසය තුළ ඇති ස්වරයක් වෙනස් නොකරයි.
+ වාර්තා නැත
+ දත්ත නැත
diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml
index a07e2ae4..a9a23b18 100644
--- a/core/presentation/src/main/res/values-tr/strings.xml
+++ b/core/presentation/src/main/res/values-tr/strings.xml
@@ -312,4 +312,6 @@
Aralık dışı CW tonlarını kaydır
DeepCW yalnızca 400-1200 Hz analiz eder. Açıkken bu aralığın dışındaki bir ton çözülmeden önce aralığa taşınır; aralıkta olan ton değiştirilmez.
+ Rapor yok
+ Veri yok
diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml
index 2e5810fe..67605704 100644
--- a/core/presentation/src/main/res/values-uk/strings.xml
+++ b/core/presentation/src/main/res/values-uk/strings.xml
@@ -171,4 +171,6 @@
Зсувати CW-тони поза діапазоном
DeepCW аналізує лише 400-1200 Гц. Якщо увімкнено, тон поза цим діапазоном переноситься в нього перед декодуванням; тон, що вже в діапазоні, не змінюється.
+ Немає звітів
+ Немає даних
diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml
index 366a0ad1..cc5733df 100644
--- a/core/presentation/src/main/res/values-zh/strings.xml
+++ b/core/presentation/src/main/res/values-zh/strings.xml
@@ -302,4 +302,6 @@
雷达罗盘偏移 (仰角)
搬移超出范围的 CW 音调
DeepCW 仅分析 400-1200 Hz。开启后,超出该范围的音调会先搬移到范围内再解码;已在范围内的音调不作处理。
+ 无人上报
+ 无数据
diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
index 84a374a2..72de5412 100644
--- a/core/presentation/src/main/res/values/strings.xml
+++ b/core/presentation/src/main/res/values/strings.xml
@@ -335,4 +335,6 @@
Radar compass offset (elev)
Shift out-of-range CW tones
DeepCW only analyses 400-1200 Hz. When on, a tone outside that range is moved into it before decoding; a tone already inside is untouched.
+ No report
+ No data
diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
index 45ff4318..e958490f 100644
--- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
+++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
@@ -167,11 +167,16 @@ private fun StatusHeader(fetchedAtUtcMs: Long, isRefreshing: Boolean, onRefresh:
/** Legend: FlowRow of colored chips — wraps to two lines on narrow screens, stays one line when wide. */
@Composable
private fun LegendRow() {
+ // The two greys are listed because most of a typical grid is grey: measured live, 81%
+ // of cells were "nobody reported" and 11% were outside the data we received. Without
+ // the distinction a mostly-grey row reads as a dead satellite.
val legend = listOf(
stringResource(id = R.string.amsat_active) to Color(0xFF648FFF),
stringResource(id = R.string.amsat_tlm) to Color(0xFFFFB000),
stringResource(id = R.string.amsat_not_heard) to Color(0xFFDC267F),
- stringResource(id = R.string.amsat_conflict) to Color(0xFFFE6100)
+ stringResource(id = R.string.amsat_conflict) to Color(0xFFFE6100),
+ stringResource(id = R.string.amsat_no_report_legend) to Color(0xFFC0C0C0),
+ stringResource(id = R.string.amsat_no_data_legend) to Color(0xFFE8E8E8)
)
FlowRow(
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp),