Restore typed local SSD server labels

ober

d5d03cbb6ef250cb29637fa6301e7c7f91e34b27

diff --git a/templates/ssd-review-parts/UiTextHelpers.ss b/templates/ssd-review-parts/UiTextHelpers.ss
index 029425b..051fc00 100644
--- a/templates/ssd-review-parts/UiTextHelpers.ss
+++ b/templates/ssd-review-parts/UiTextHelpers.ss
@@ -1819,6 +1819,19 @@
             Unit
             (kotlin-call mainServerDetectFirst))
           (extern
+            (mainActivityFetchServerLabelsRaw
+              (activity : MainActivity)
+              (bitmap : Bitmap)
+              (uri : Uri)
+              (generation : Int32)
+              (page : Int32)
+              (pageCount : Int32)
+              (name : String)
+              (dpi : Int32))
+            :
+            Unit
+            (kotlin-call mainServerFetchLocalLabelsAsync))
+          (extern
             (mainActivitySelfTestLogRaw
               (activity : MainActivity)
               (stage : String))
@@ -1858,6 +1871,30 @@
             Unit
             (kotlin-call sendClientLogBlocking))
           (extern
+            (mainActivityFilesDirRaw (activity : MainActivity))
+            :
+            File
+            (kotlin-member-get filesDir))
+          (extern
+            (mainActivityLocalLogFileRaw
+              (directory : File)
+              (name : String))
+            :
+            File
+            (kotlin-call File))
+          (extern
+            (mainActivityAppendTextRaw
+              (file : File)
+              (text : String))
+            :
+            Unit
+            (kotlin-member-call appendText))
+          (extern
+            (mainActivityJsonTextRaw (payload : JSONObject))
+            :
+            String
+            (kotlin-member-call toString))
+          (extern
             (throwableLogJsonForExceptionRaw (error : Exception))
             :
             JSONObject
@@ -6126,13 +6163,24 @@
                                      (nullable-none JSONObject)))])
                  (if (nullable-null? payload)
                      (begin)
-                     (runBackground
-                       (lambda ()
-                         (try (mainActivitySendClientLogRaw
-                                activity
-                                (nullable-get payload)
-                                readTimeoutMillis)
-                              (catch (error : Exception) (begin))))))))
+                     (begin
+                       (try
+                         (mainActivityAppendTextRaw
+                           (mainActivityLocalLogFileRaw
+                             (mainActivityFilesDirRaw activity)
+                             "ssd-review-client-local.jsonl")
+                           (string-append
+                             (mainActivityJsonTextRaw
+                               (nullable-get payload))
+                             "\n"))
+                         (catch (error : Exception) (begin)))
+                       (runBackground
+                         (lambda ()
+                           (try (mainActivitySendClientLogRaw
+                                  activity
+                                  (nullable-get payload)
+                                  readTimeoutMillis)
+                                (catch (error : Exception) (begin)))))))))
           (def (mainActivitySaveTruthSnapshotAsync (activity : MainActivity) (view : SsdReviewView)
                  (store : TruthStore) (truth : JSONObject)
                  (viewport : (Nullable ViewportState))
@@ -6497,7 +6545,11 @@
                                       (mainActivityDpiValue)
                                       (mainActivityFastTruthGroupThresholdValue)
                                       (int32 120)
-                                      (mainActivityClientLogReadTimeoutMillisValue)))))))
+                                      (mainActivityClientLogReadTimeoutMillisValue))
+                                    (mainActivityFetchServerLabelsRaw
+                                      activity bitmap uri renderGeneration
+                                      renderPageIndex renderPageCount
+                                      renderPdfName (mainActivityDpiValue)))))))
                         (catch
                           (error : Exception)
                           (mainActivityRunOnUiThreadRaw
diff --git a/templates/ssd-review-parts/main-server-detect.ss b/templates/ssd-review-parts/main-server-detect.ss
index 291fe10..f87bdf0 100644
--- a/templates/ssd-review-parts/main-server-detect.ss
+++ b/templates/ssd-review-parts/main-server-detect.ss
@@ -6,11 +6,12 @@
                       (android net Uri)
                       (org json JSONObject))
       (typed-library (com sfb ssdreview)
-        (export mainServerDetectFirst)
+        (export mainServerDetectFirst mainServerFetchLocalLabelsAsync)
         (type Bitmap)
         (type Exception)
         (type Int32)
         (type MainActivity)
+        (type MainWorkToken)
         (type JSONObject)
         (type SfbServerClient)
         (type SfbServerPdf)
@@ -61,6 +62,20 @@
                   (activity : MainActivity)
                   (status : String)) : Unit
           (kotlin-call mainActivitySetStatusRaw))
+        (extern (mainServerDetectWorkBegin
+                  (activity : MainActivity)
+                  (label : String)) : MainWorkToken
+          (kotlin-call mainWorkBegin))
+        (extern (mainServerDetectWorkDone
+                  (activity : MainActivity)
+                  (token : MainWorkToken)
+                  (label : String)) : Unit
+          (kotlin-call mainWorkDone))
+        (extern (mainServerDetectWorkFailed
+                  (activity : MainActivity)
+                  (token : MainWorkToken)
+                  (label : String)) : Unit
+          (kotlin-call mainWorkFailed))
         (extern (mainServerDetectFindPdf
                   (client : SfbServerClient)
                   (name : String)) : (Nullable SfbServerPdf)
@@ -68,6 +83,9 @@
         (extern (mainServerDetectMatchedPdfId
                   (pdf : SfbServerPdf)) : String
           (kotlin-member-get pdfId))
+        (extern (mainServerDetectEndpointLabel
+                  (client : SfbServerClient)) : String
+          (kotlin-member-call endpointLabel))
         (extern (mainServerDetectFetchRecognition
                   (client : SfbServerClient)
                   (pdfId : String)
@@ -104,6 +122,14 @@
         (extern (mainServerDetectRefresh
                   (view : SsdReviewView)) : Unit
           (kotlin-call reviewViewRefresh))
+        (extern (mainServerDetectGroupId
+                  (group : SsdGroup)) : String
+          (kotlin-member-get id))
+        (extern (mainServerDetectSelectGroup
+                  (activity : MainActivity)
+                  (view : SsdReviewView)
+                  (groupId : (Nullable String))) : Unit
+          (kotlin-call mainActivitySelectGroupId))
         (extern (mainServerDetectLogResult
                   (activity : MainActivity)
                   (stage : String)
@@ -159,6 +185,21 @@
                 "error"
                 (mainServerDetectErrorText error))
               fields)))
+        (def (mainServerLocalFetchFields
+               (client : SfbServerClient)
+               (pdfId : String)
+               (page : Int32)
+               (groupCount : Int32)) : JSONObject
+          (let ((fields (json-object-empty)))
+            (begin
+              (json-object-put-string!
+                fields "endpoint" (mainServerDetectEndpointLabel client))
+              (json-object-put-string! fields "pdf_id" pdfId)
+              (json-object-put-int32! fields "page" page)
+              (json-object-put-int32!
+                fields "recognition_groups" groupCount)
+              (json-object-put-int32! fields "truth_groups" (int32 0))
+              fields)))
         (def (mainServerDetectResolvePdfId
                (activity : MainActivity)
                (client : SfbServerClient)
@@ -231,6 +272,115 @@
         (extern (mainServerDetectRecognitionGroupsForSession
                   (session : SsdSession)) : (List SsdGroup)
           (kotlin-member-get groups))
+        (def (mainServerApplyLocalLabels
+               (activity : MainActivity)
+               (token : MainWorkToken)
+               (generation : Int32)
+               (page : Int32)
+               (uri : Uri)
+               (session : SsdSession)) : Unit
+          (if (mainServerDetectCurrent activity generation uri page)
+            (let ((view (mainServerDetectView activity))
+                  (groups
+                    (mainServerDetectRecognitionGroupsForSession session)))
+              (if (= (list-size groups) (int32 0))
+                (begin
+                  (mainServerDetectSetStatus
+                    activity "Server labels returned no groups")
+                  (mainServerDetectWorkFailed
+                    activity token "Server labels returned no groups"))
+                (begin
+                  (mainServerDetectShowSession activity view session)
+                  (mainServerDetectSelectGroup
+                    activity view
+                    (nullable-some
+                      (mainServerDetectGroupId
+                        (list-ref groups (int32 0)))))
+                  (mainServerDetectRefresh view)
+                  (mainServerDetectSetStatus
+                    activity
+                    (string-append
+                      "Server labels applied: "
+                      (string-append
+                        (int32->string (list-size groups)) " groups")))
+                  (mainServerDetectWorkDone
+                    activity token
+                    (string-append
+                      "Loaded server labels: "
+                      (string-append
+                        (int32->string (list-size groups)) " groups")))
+                  (mainServerDetectLogResult
+                    activity "server-labels-for-local" session
+                    "sfb-server" (int32 0) (int32 0)))))
+            (mainServerDetectWorkFailed
+              activity token "Server labels superseded")))
+        (def (mainServerFetchLocalLabelsAsync
+               (activity : MainActivity)
+               (bitmap : Bitmap)
+               (uri : Uri)
+               (generation : Int32)
+               (page : Int32)
+               (pageCount : Int32)
+               (name : String)
+               (dpi : Int32)) : Unit
+          (let ((token
+                  (mainServerDetectWorkBegin
+                    activity "Loading server labels")))
+            (mainServerDetectRunBackground
+              "sfb-server-local-labels"
+              (lambda ()
+                (try
+                  (let ((client (mainServerDetectClient activity)))
+                    (let ((pdfId
+                            (mainServerDetectResolvePdfId
+                              activity client name)))
+                      (if (string-blank? pdfId)
+                        (throw
+                          (new IllegalStateException
+                            (string-append
+                              "No matching server SSD PDF for " name))
+                          Unit)
+                        (let ((recognition
+                                (mainServerDetectFetchRecognition
+                                  client pdfId (+ page (int32 1)) dpi)))
+                          (let ((groupCount
+                                  (list-size
+                                    (mainServerDetectRecognitionGroups
+                                      recognition))))
+                            (begin
+                              (mainServerDetectLogEvent
+                                activity
+                                "server-page-label-fetch"
+                                (mainServerLocalFetchFields
+                                  client pdfId (+ page (int32 1)) groupCount))
+                              (let ((session
+                                      (mainServerDetectRecognitionSession
+                                        pdfId name uri page pageCount bitmap
+                                        recognition
+                                        (nullable-none FloatArray))))
+                                (mainServerDetectRunOnUi
+                                  activity
+                                  (lambda ()
+                                    (mainServerApplyLocalLabels
+                                      activity token generation page uri
+                                      session))))))))))
+                  (catch (error : Exception)
+                    (mainServerDetectRunOnUi
+                      activity
+                      (lambda ()
+                        (if (mainServerDetectCurrent
+                              activity generation uri page)
+                          (let ((message
+                                  (string-append
+                                    "Server labels failed: "
+                                    (mainServerDetectErrorText error))))
+                            (begin
+                              (mainServerDetectSetStatus activity message)
+                              (mainServerDetectWorkFailed
+                                activity token message)))
+                          (mainServerDetectWorkFailed
+                            activity token "Server labels superseded"))))))))))
+        (type IllegalStateException)
         (def (mainServerDetectFirst
                (activity : MainActivity)
                (dpi : Int32)