Log SSD detection results and rerender after seed import

ober

c483b796892931954c764d0d7d283889f5320c48

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 6bdf90c..fd06b41 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -349,6 +349,7 @@
        "            if (imported > 0) {"
        "                runOnUiThread {"
        "                    setStatus(\"Truth seed merged $imported files. ${truthStore.storageSummary()}\")"
+       "                    if (pdfUri != null) renderCurrentPage()"
        "                }"
        "            }"
        "        }"
@@ -921,6 +922,7 @@
 	       "                        reviewView.selectedGroupId = null"
 	       "                        reviewView.ssdArea = session.ssdArea"
 	       "                        setStatus(\"${session.cells.size} cells, ${session.groups.size} groups, replayed cached truth\")"
+	       "                        logDetectResult(\"cached-truth\", session, \"local-truth\")"
        "                    }"
        "                    truthStore.pullRemoteTruth(session)"
        "                    return@thread"
@@ -930,12 +932,19 @@
        "                val existingTruth = truthStore.loadTruth(session)"
        "                var ocrGuesses = 0"
        "                var detectorNote = \"\""
+       "                var detectorSource = \"unknown\""
        ""
 	       "                if (existingTruth != null) {"
+	       "                    detectorSource = \"local-truth\""
 	       "                    session.ssdArea = jsonRect(existingTruth.optJSONArray(\"ssd_area\"))"
 	       "                    if (forceDetect) {"
        "                        val forcedDetection = SsdVisionBridge.detect(bitmap)"
-       "                        if (forcedDetection != null) detectorNote = \", rust detector\""
+       "                        if (forcedDetection != null) {"
+       "                            detectorNote = \", rust detector\""
+       "                            detectorSource = \"forced-rust-plus-truth\""
+       "                        } else {"
+       "                            detectorSource = \"forced-android-plus-truth\""
+       "                        }"
        "                        val forcedCells = forcedDetection?.cells ?: SsdDetector.detectCells(bitmap)"
        "                        session.cells.addAll(forcedCells.filter { session.cellInsideSsdArea(it) })"
 	       "                    }"
@@ -947,9 +956,11 @@
        "                        session.cells.addAll(nativeDetection.cells)"
        "                        session.groups.addAll(nativeDetection.groups.take(80))"
        "                        detectorNote = \", rust detector\""
+       "                        detectorSource = \"rust-detector\""
        "                    } else {"
        "                        session.cells.addAll(SsdDetector.detectCells(bitmap))"
        "                        session.groups.addAll(SsdDetector.initialGroups(session.cells).take(80))"
+       "                        detectorSource = \"android-fallback\""
        "                    }"
        "                    Guessing.applyInitialGuesses(this, session)"
        "                    session.ocrWords.addAll(SsdOcr.recognizeWords(bitmap))"
@@ -967,12 +978,19 @@
        "                    val ocrNote = if (ocrGuesses > 0) \", $ocrGuesses OCR guesses\" else if (session.ocrWords.isNotEmpty()) \", OCR ${session.ocrWords.size} words\" else \"\""
        "                    val learnedNote = if (learnedGuesses > 0) \", $learnedGuesses learned guesses\" else \"\""
        "                    setStatus(\"${session.cells.size} cells, ${session.groups.size} groups$detectorNote$truthNote$ocrNote$learnedNote\")"
+       "                    logDetectResult(\"page-load\", session, detectorSource, learnedGuesses, ocrGuesses)"
        "                    if (existingTruth == null && !forceDetect) mergeRemoteTruthAsync(session, expectedGeneration, uri, expectedPageIndex)"
        "                }"
        "            } catch (error: Exception) {"
        "                runOnUiThread {"
        "                    if (isCurrentLoad(expectedGeneration, uri, expectedPageIndex)) {"
        "                        setStatus(\"Detect failed: ${error.message}\")"
+       "                        logClientEvent(\"ssd-detect-failed\", JSONObject()"
+       "                            .put(\"source_key\", sourceKey(expectedPdfSha1, expectedPageIndex + 1, DPI))"
+       "                            .put(\"force\", forceDetect)"
+       "                            .put(\"native_available\", SsdVisionBridge.isAvailable())"
+       "                            .put(\"native_error\", SsdVisionBridge.loadErrorMessage())"
+       "                            .put(\"error\", throwableLog(error)))"
        "                    }"
        "                }"
        "            }"
@@ -1002,6 +1020,7 @@
        "                reviewView.ssdArea = merged.ssdArea"
        "                reviewView.invalidate()"
        "                setStatus(\"${merged.cells.size} cells, ${merged.groups.size} groups, pulled remote truth\")"
+       "                logDetectResult(\"remote-truth\", merged, \"remote-truth\")"
        "            }"
        "        }"
        "    }"
@@ -1796,6 +1815,33 @@
        "        }"
        "    }"
        ""
+       "    private fun logDetectResult("
+       "        stage: String,"
+       "        session: SsdSession,"
+       "        detectorSource: String,"
+       "        learnedGuesses: Int = 0,"
+       "        ocrGuesses: Int = 0"
+       "    ) {"
+       "        logClientEvent(\"ssd-detect-result\", JSONObject()"
+       "            .put(\"stage\", stage)"
+       "            .put(\"detector_source\", detectorSource)"
+       "            .put(\"source_key\", session.sourceKey)"
+       "            .put(\"source_name\", session.sourceName)"
+       "            .put(\"page\", session.page)"
+       "            .put(\"dpi\", session.dpi)"
+       "            .put(\"image_width\", session.imageWidth)"
+       "            .put(\"image_height\", session.imageHeight)"
+       "            .put(\"cells\", session.cells.size)"
+       "            .put(\"groups\", session.groups.size)"
+       "            .put(\"suppressed_groups\", session.suppressedGroups.size)"
+       "            .put(\"ocr_words\", session.ocrWords.size)"
+       "            .put(\"ocr_guesses\", ocrGuesses)"
+       "            .put(\"learned_guesses\", learnedGuesses)"
+       "            .put(\"ssd_area\", session.ssdArea != null)"
+       "            .put(\"native_available\", SsdVisionBridge.isAvailable())"
+       "            .put(\"native_error\", SsdVisionBridge.loadErrorMessage()))"
+       "    }"
+       ""
        "    private fun sendClientLog(payload: JSONObject, readTimeoutMillis: Int) {"
        "        val bytes = payload.toString().toByteArray(Charsets.UTF_8)"
        "        val connection = (URL(CLIENT_LOG_ENDPOINT).openConnection() as HttpURLConnection).apply {"