From 4d32b5b3da712634d542cf7e0174dea69d9b9889 Mon Sep 17 00:00:00 2001 From: rayvader <42436531+rayvader@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:49:45 +0000 Subject: [PATCH] fix: Froyo widget reads the live list from the fold row's data The gateway's custom fold row for open_items_live is {data: {items, as_of}, at, writer, stale_after}. The widget read open_items_live.items, found nothing, and fell back to Froyo's hand-written open_items, which is stale (no snorkel trip). Read data.items first, the way Froyo's own page does; keep bare items as a fallback. Adds tests for the real row shape. --- .../github/rayvader/kirocrew/core/Upcoming.kt | 12 ++++++++--- .../rayvader/kirocrew/core/UpcomingTest.kt | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/github/rayvader/kirocrew/core/Upcoming.kt b/app/src/main/java/io/github/rayvader/kirocrew/core/Upcoming.kt index 4f0cde4..8e332f4 100644 --- a/app/src/main/java/io/github/rayvader/kirocrew/core/Upcoming.kt +++ b/app/src/main/java/io/github/rayvader/kirocrew/core/Upcoming.kt @@ -71,8 +71,9 @@ object Upcoming { const val FIELD = "open_items" /** - * The field Froyo's sync job keeps current every 15 min: an object whose `items` is the - * same rows as [FIELD]. Preferred when it holds a list; [FIELD] is the fallback. + * The field Froyo's sync job keeps current every 15 min: the gateway's custom fold row + * {data: {items, as_of}, at, stale_after}, whose `data.items` is the same rows as [FIELD]. + * Preferred when it holds a list; [FIELD] is the fallback. */ const val LIVE_FIELD = "open_items_live" private const val MAX_TITLE = 200 @@ -126,7 +127,12 @@ object Upcoming { val html = JSONObject(body).optString("rendered_html") val island = island(html) ?: return null val fields = JSONObject(island).optJSONObject("fields") ?: return null - val live = fields.optJSONObject(LIVE_FIELD)?.optJSONArray("items") + // The gateway's custom fold row is {data: {items, as_of}, at, stale_after}: the + // sync job's record sits under `data`, which is where the page itself reads it. + // A bare {items} is still read, in case a page binds `.data` directly. + val liveObj = fields.optJSONObject(LIVE_FIELD) + val live = liveObj?.optJSONObject("data")?.optJSONArray("items") + ?: liveObj?.optJSONArray("items") (live ?: fields.optJSONArray(FIELD))?.toString() } catch (e: JSONException) { null diff --git a/app/src/test/java/io/github/rayvader/kirocrew/core/UpcomingTest.kt b/app/src/test/java/io/github/rayvader/kirocrew/core/UpcomingTest.kt index 912f569..362383e 100644 --- a/app/src/test/java/io/github/rayvader/kirocrew/core/UpcomingTest.kt +++ b/app/src/test/java/io/github/rayvader/kirocrew/core/UpcomingTest.kt @@ -136,6 +136,27 @@ class UpcomingTest { assertEquals(listOf("Test dinner 18:30", "Test flight"), items.map { it.title }) } + /** The real shape: the gateway's custom fold row wraps the job's record in `data`. */ + @Test + fun theLiveListIsReadFromTheFoldRowsData() { + val old = """[{"n":1,"id":"a","title":"Old item","due":"2026-11-04","kind":"booking","flag":null}]""" + val live = """{"data":{"items":[{"n":1,"id":"b","title":"Test dinner 18:30","due":"2026-11-05","kind":"booking"},""" + + """{"n":2,"id":"c","title":"Test snorkel 14:30","due":"2026-11-06","kind":"booking"}],""" + + """"as_of":"Thu 5 Nov, 21:28 SGT ยท 2 open"},"at":"2026-11-05T13:28:55Z",""" + + """"writer":"4cbe3e0f","stale_after":"2026-11-05T14:28:55Z"}""" + val items = Upcoming.parseItems(Upcoming.extractItems(v4Dashboard(live, old))!!, zone) + assertEquals(listOf("Test dinner 18:30", "Test snorkel 14:30"), items.map { it.title }) + } + + @Test + fun aFoldRowWithNoListInItsDataFallsBackToOpenItems() { + val old = """[{"n":1,"title":"Only item","due":null,"kind":"bill","flag":"x"}]""" + val live = """{"data":{"as_of":"x"},"at":"2026-11-05T13:28:55Z"}""" + assertEquals(listOf("Only item"), Upcoming.parseItems(Upcoming.extractItems(v4Dashboard(live, old))!!, zone).map { it.title }) + // An empty list under data is a real answer: nothing open. + assertEquals("[]", Upcoming.extractItems(v4Dashboard("""{"data":{"items":[]}}""", old))) + } + @Test fun aV4PageWithoutTheLiveListFallsBackToOpenItems() { val old = """[{"n":1,"title":"Only item","due":null,"kind":"bill","flag":"x"}]"""