Integrate typed SSD server page flow

ober

d5f566b73ed1699eb7b061c45016c754d5fe89c2

diff --git a/templates/ssd-review-parts/main-server-pages.ss b/templates/ssd-review-parts/main-server-pages.ss
new file mode 100644
index 0000000..70c7c6a
--- /dev/null
+++ b/templates/ssd-review-parts/main-server-pages.ss
@@ -0,0 +1,891 @@
+(import (jerboa prelude))
+
+(def fragment
+  '((typed-kotlin-file "com/sfb/ssdreview/MainServerPages.kt"
+      (kotlin-imports (android app AlertDialog)
+                      (android app AlertDialog Builder)
+                      (android content Context)
+                      (android content DialogInterface)
+                      (android content SharedPreferences)
+                      (android graphics Bitmap)
+                      (android graphics BitmapFactory)
+                      (android net Uri)
+                      (android text Editable)
+                      (android widget EditText)
+                      (java util UUID)
+                      (org json JSONObject))
+      (typed-library (com sfb ssdreview)
+        (export mainServerUri mainServerOpenPdfs
+                mainServerFindEndpointAsync mainServerShowEndpointDialog
+                mainServerRenderCurrentPage mainServerRestorePdf)
+        (type Bitmap)
+        (type Builder)
+        (type DialogInterface)
+        (type EditText)
+        (type Editable)
+        (type Exception)
+        (type FloatArray)
+        (type Int32)
+        (type MainActivity)
+        (type OcrWord)
+        (type SfbServerCatalog)
+        (type SfbServerClient)
+        (type SfbServerInfo)
+        (type SfbServerManifest)
+        (type SfbServerPageTruth)
+        (type SfbServerPdf)
+        (type SfbServerRecognition)
+        (type SfbServerSnapshot)
+        (type SharedPreferences)
+        (type SsdCell)
+        (type SsdGroup)
+        (type SsdReviewView)
+        (type SsdSession)
+        (type Uri)
+        (type UUID)
+        (type ViewportState)
+        (extern (mainServerPageDpi) : Int32
+          (kotlin-value DPI))
+        (extern (mainServerPageClient
+                  (activity : MainActivity)) : SfbServerClient
+          (kotlin-member-get serverClient))
+        (extern (mainServerPageReviewView
+                  (activity : MainActivity)) : SsdReviewView
+          (kotlin-member-get reviewView))
+        (extern (mainServerPageUri
+                  (activity : MainActivity)) : (Nullable Uri)
+          (kotlin-member-get pdfUri))
+        (extern (mainServerPageUriSet
+                  (activity : MainActivity)
+                  (uri : (Nullable Uri))) : Unit
+          (kotlin-member-set pdfUri))
+        (extern (mainServerPagePdfName
+                  (activity : MainActivity)) : String
+          (kotlin-member-get pdfName))
+        (extern (mainServerPagePdfNameSet
+                  (activity : MainActivity)
+                  (name : String)) : Unit
+          (kotlin-member-set pdfName))
+        (extern (mainServerPagePdfSha1Set
+                  (activity : MainActivity)
+                  (sha1 : String)) : Unit
+          (kotlin-member-set pdfSha1))
+        (extern (mainServerPagePdfIdSet
+                  (activity : MainActivity)
+                  (pdfId : String)) : Unit
+          (kotlin-member-set serverPdfId))
+        (extern (mainServerPageCurrentPdfId
+                  (activity : MainActivity)) : String
+          (kotlin-member-get serverPdfId))
+        (extern (mainServerPageIndex
+                  (activity : MainActivity)) : Int32
+          (kotlin-member-get pageIndex))
+        (extern (mainServerPageIndexSet
+                  (activity : MainActivity)
+                  (page : Int32)) : Unit
+          (kotlin-member-set pageIndex))
+        (extern (mainServerPageCount
+                  (activity : MainActivity)) : Int32
+          (kotlin-member-get pageCount))
+        (extern (mainServerPageCountSet
+                  (activity : MainActivity)
+                  (count : Int32)) : Unit
+          (kotlin-member-set pageCount))
+        (extern (mainServerPageLoadGeneration
+                  (activity : MainActivity)) : Int32
+          (kotlin-member-get loadGeneration))
+        (extern (mainServerPageAdvanceGeneration
+                  (activity : MainActivity)
+                  (generation : Int32)) : Int32
+          (kotlin-call mainActivityAdvanceLoadGeneration))
+        (extern (mainServerPagePersist
+                  (activity : MainActivity)
+                  (includeViewport : Bool)) : Unit
+          (kotlin-call mainActivityPersistCurrentPdfState))
+        (extern (mainServerPageRenderCurrent
+                  (activity : MainActivity)) : Unit
+          (kotlin-call mainActivityRenderCurrentPage))
+        (extern (mainServerPageRunBackground
+                  (name : String)
+                  (action : (-> Unit))) : Unit
+          (kotlin-call runBackgroundNamed))
+        (extern (mainServerPageRunOnUi
+                  (activity : MainActivity)
+                  (action : (-> Unit))) : Unit
+          (kotlin-member-call runOnUiThread))
+        (extern (mainServerPageSetStatus
+                  (activity : MainActivity)
+                  (text : String)) : Unit
+          (kotlin-call mainActivitySetStatusRaw))
+        (extern (mainServerPageCurrentLoad
+                  (expectedGeneration : Int32)
+                  (actualGeneration : Int32)
+                  (actualUri : (Nullable Uri))
+                  (expectedUri : Uri)
+                  (actualPage : Int32)
+                  (expectedPage : Int32)) : Bool
+          (kotlin-call currentLoadMatches))
+        (extern (mainServerPageRestoredViewport
+                  (activity : MainActivity)
+                  (uri : Uri)
+                  (page : Int32)) : (Nullable ViewportState)
+          (kotlin-call mainActivityRestoredViewportFor))
+        (extern (mainServerPageRestoreViewport
+                  (view : SsdReviewView)
+                  (viewport : (Nullable ViewportState))) : Unit
+          (kotlin-call reviewViewRestoreViewportAfterLayout))
+        (extern (mainServerPageShowBitmap
+                  (activity : MainActivity)
+                  (view : SsdReviewView)
+                  (bitmap : Bitmap)) : Unit
+          (kotlin-call mainActivityShowBitmapAndClearSession))
+        (extern (mainServerPageShowSession
+                  (activity : MainActivity)
+                  (view : SsdReviewView)
+                  (session : SsdSession)) : Unit
+          (kotlin-call mainActivityShowSession))
+        (extern (mainServerPageReviewRefresh
+                  (view : SsdReviewView)) : Unit
+          (kotlin-call reviewViewRefresh))
+        (extern (mainServerPageUriParse (text : String)) : Uri
+          (kotlin-call Uri parse))
+        (extern (mainServerPageUriScheme
+                  (uri : Uri)) : (Nullable String)
+          (kotlin-member-get scheme))
+        (extern (mainServerPageUriText (uri : Uri)) : String
+          (kotlin-member-call toString))
+        (extern (mainServerPageUriSpecificPart
+                  (uri : Uri)) : (Nullable String)
+          (kotlin-member-get schemeSpecificPart))
+        (extern (mainServerPageModePrivate) : Int32
+          (kotlin-value Context MODE_PRIVATE))
+        (extern (mainServerPageActivityPreferences
+                  (activity : MainActivity)
+                  (mode : Int32)) : SharedPreferences
+          (kotlin-member-call getPreferences))
+        (extern (mainServerPagePreferenceString
+                  (preferences : SharedPreferences)
+                  (key : String)
+                  (fallback : (Nullable String))) : (Nullable String)
+          (kotlin-member-call getString))
+        (extern (mainServerPagePreferenceInt
+                  (preferences : SharedPreferences)
+                  (key : String)
+                  (fallback : Int32)) : Int32
+          (kotlin-member-call getInt))
+        (extern (mainServerPagePrefName) : String
+          (kotlin-value PREF_LAST_PDF_NAME))
+        (extern (mainServerPagePrefCount) : String
+          (kotlin-value PREF_LAST_PAGE_COUNT))
+        (extern (mainServerPageUuid) : UUID
+          (kotlin-call UUID randomUUID))
+        (extern (mainServerPageUuidText (uuid : UUID)) : String
+          (kotlin-member-call toString))
+        (extern (mainServerPageSessionId (uuid : String)) : String
+          (kotlin-call sessionIdFromUuid))
+        (extern (mainServerPageSourceKey
+                  (pdfId : String)
+                  (page : Int32)
+                  (dpi : Int32)) : String
+          (kotlin-call sourceKey))
+        (extern (mainServerPageMakeSession
+                  (sessionId : String)
+                  (sourceKey : String)
+                  (sourceName : String)
+                  (sourceUri : String)
+                  (page : Int32)
+                  (pageCount : Int32)
+                  (dpi : Int32)
+                  (imageWidth : Int32)
+                  (imageHeight : Int32)) : SsdSession
+          (kotlin-call makeDetectionSession))
+        (extern (mainServerPageBitmapWidth
+                  (bitmap : Bitmap)) : Int32
+          (kotlin-member-get width))
+        (extern (mainServerPageBitmapHeight
+                  (bitmap : Bitmap)) : Int32
+          (kotlin-member-get height))
+        (extern (mainServerPageBytesSize (bytes : Bytes)) : Int32
+          (kotlin-member-get size))
+        (extern (mainServerPageDecodeBitmap
+                  (bytes : Bytes)
+                  (offset : Int32)
+                  (length : Int32)) : (Nullable Bitmap)
+          (kotlin-call BitmapFactory decodeByteArray))
+        (extern (mainServerPageClientCatalog
+                  (client : SfbServerClient)) : SfbServerCatalog
+          (kotlin-member-call fetchServerPdfs))
+        (extern (mainServerPageClientRefresh
+                  (client : SfbServerClient)) : SfbServerInfo
+          (kotlin-member-call refreshServerEndpoint))
+        (extern (mainServerPageClientEndpoint
+                  (client : SfbServerClient)) : String
+          (kotlin-member-call endpointLabel))
+        (extern (mainServerPageClientManifest
+                  (client : SfbServerClient)
+                  (pdfId : String)
+                  (page : Int32)
+                  (dpi : Int32)) : SfbServerManifest
+          (kotlin-member-call fetchServerManifest))
+        (extern (mainServerPageClientSnapshot
+                  (client : SfbServerClient)
+                  (pdfId : String)
+                  (page : Int32)
+                  (dpi : Int32)) : SfbServerSnapshot
+          (kotlin-member-call fetchServerSnapshot))
+        (extern (mainServerPageClientTruth
+                  (client : SfbServerClient)
+                  (pdfId : String)
+                  (page : Int32)
+                  (dpi : Int32)) : SfbServerPageTruth
+          (kotlin-member-call fetchServerPageTruth))
+        (extern (mainServerPageClientRecognition
+                  (client : SfbServerClient)
+                  (pdfId : String)
+                  (page : Int32)
+                  (dpi : Int32)) : SfbServerRecognition
+          (kotlin-member-call fetchServerRecognition))
+        (extern (mainServerPageCatalogPdfs
+                  (catalog : SfbServerCatalog)) : (List SfbServerPdf)
+          (kotlin-member-get pdfs))
+        (extern (mainServerPagePdfId
+                  (pdf : SfbServerPdf)) : String
+          (kotlin-member-get pdfId))
+        (extern (mainServerPagePdfLabel
+                  (pdf : SfbServerPdf)) : String
+          (kotlin-member-get name))
+        (extern (mainServerPagePdfPages
+                  (pdf : SfbServerPdf)) : Int32
+          (kotlin-member-get pageCount))
+        (extern (mainServerPageManifestArea
+                  (manifest : SfbServerManifest)) : (Nullable FloatArray)
+          (kotlin-member-get ssdArea))
+        (extern (mainServerPageSnapshotBytes
+                  (snapshot : SfbServerSnapshot)) : Bytes
+          (kotlin-member-get bytes))
+        (extern (mainServerPageTruthJson
+                  (truth : SfbServerPageTruth)) : (Nullable JSONObject)
+          (kotlin-member-get truth))
+        (type JSONObject)
+        (extern (mainServerPageRecognitionArea
+                  (recognition : SfbServerRecognition)) : (Nullable FloatArray)
+          (kotlin-member-get ssdArea))
+        (extern (mainServerPageRecognitionGroups
+                  (recognition : SfbServerRecognition)) : (List SsdGroup)
+          (kotlin-member-get groups))
+        (extern (mainServerPageRecognitionCells
+                  (recognition : SfbServerRecognition)) : (List SsdCell)
+          (kotlin-member-get cells))
+        (extern (mainServerPageRecognitionWords
+                  (recognition : SfbServerRecognition)) : (List OcrWord)
+          (kotlin-member-get ocrWords))
+        (extern (mainServerPageSessionCells
+                  (session : SsdSession)) : (MutableList SsdCell)
+          (kotlin-member-get cells))
+        (extern (mainServerPageSessionGroups
+                  (session : SsdSession)) : (MutableList SsdGroup)
+          (kotlin-member-get groups))
+        (extern (mainServerPageSessionWords
+                  (session : SsdSession)) : (MutableList OcrWord)
+          (kotlin-member-get ocrWords))
+        (extern (mainServerPageSessionArea
+                  (session : SsdSession)) : (Nullable FloatArray)
+          (kotlin-member-get ssdArea))
+        (extern (mainServerPageSessionAreaSet
+                  (session : SsdSession)
+                  (area : (Nullable FloatArray))) : Unit
+          (kotlin-member-set ssdArea))
+        (extern (mainServerPageApplyTruth
+                  (session : SsdSession)
+                  (truth : JSONObject)) : Unit
+          (kotlin-call applyTruthJsonToSession))
+        (extern (mainServerPagePrune
+                  (session : SsdSession)) : Int32
+          (kotlin-call ssdSessionPruneGroupsOutsideSsdArea))
+        (extern (mainServerPageBuilder
+                  (activity : MainActivity)) : Builder
+          (kotlin-call AlertDialog Builder))
+        (extern (mainServerPageBuilderTitle
+                  (builder : Builder)
+                  (title : String)) : Builder
+          (kotlin-member-call setTitle))
+        (extern (mainServerPageBuilderMessage
+                  (builder : Builder)
+                  (message : String)) : Builder
+          (kotlin-member-call setMessage))
+        (extern (mainServerPageBuilderView
+                  (builder : Builder)
+                  (view : EditText)) : Builder
+          (kotlin-member-call setView))
+        (extern (mainServerPageBuilderItems
+                  (builder : Builder)
+                  (items : (Array String))
+                  (listener : (-> DialogInterface Int32 Unit))) : Builder
+          (kotlin-member-call setItems))
+        (extern (mainServerPageBuilderNegative
+                  (builder : Builder)
+                  (label : String)
+                  (listener : (-> DialogInterface Int32 Unit))) : Builder
+          (kotlin-member-call setNegativeButton))
+        (extern (mainServerPageBuilderPositive
+                  (builder : Builder)
+                  (label : String)
+                  (listener : (-> DialogInterface Int32 Unit))) : Builder
+          (kotlin-member-call setPositiveButton))
+        (extern (mainServerPageBuilderShow (builder : Builder)) : AlertDialog
+          (kotlin-member-call show))
+        (type AlertDialog)
+        (extern (mainServerPageStringArray
+                  (items : (List String))) : (Array String)
+          (kotlin-member-call toTypedArray))
+        (extern (mainServerPageEditText
+                  (activity : MainActivity)) : EditText
+          (kotlin-call EditText))
+        (extern (mainServerPageEditTextSet
+                  (view : EditText)
+                  (text : String)) : Unit
+          (kotlin-member-call setText))
+        (extern (mainServerPageEditTextEditable
+                  (view : EditText)) : Editable
+          (kotlin-member-call getText))
+        (extern (mainServerPageEditableText
+                  (text : Editable)) : String
+          (kotlin-member-call toString))
+        (extern (mainServerPageExceptionMessage
+                  (error : Exception)) : (Nullable String)
+          (kotlin-member-get message))
+        (extern (mainServerPageEndpointLabel
+                  (activity : MainActivity)) : String
+          (kotlin-call mainServerEndpointLabel))
+        (extern (mainServerPageSetEndpoint
+                  (activity : MainActivity)
+                  (endpoint : String)) : Unit
+          (kotlin-call mainServerSetEndpoint))
+        (def (mainServerPageErrorText (error : Exception)) : String
+          (let ((message (mainServerPageExceptionMessage error)))
+            (if (nullable-null? message)
+              "unknown"
+              (nullable-get message))))
+        (def (mainServerUri (uri : Uri)) : Bool
+          (let ((scheme (mainServerPageUriScheme uri)))
+            (and (not (nullable-null? scheme))
+                 (equal? (string-lowercase (nullable-get scheme)) "sfbserver"))))
+        (def (mainServerPositiveCount (value : Int32)) : Int32
+          (if (< value (int32 1)) (int32 1) value))
+        (def (mainServerNullableString
+               (value : (Nullable String))
+               (fallback : String)) : String
+          (if (nullable-null? value)
+            fallback
+            (let ((text (string-trim (nullable-get value))))
+              (if (string-blank? text) fallback text))))
+        (def (mainServerRestoredPage
+               (page : Int32)
+               (count : Int32)) : Int32
+          (if (< page (int32 0))
+            (int32 0)
+            (if (>= page count) (- count (int32 1)) page)))
+        (def (mainServerRestorePdf
+               (activity : MainActivity)
+               (uri : Uri)
+               (page : Int32)) : Bool
+          (let ((pdfId
+                  (string-remove-prefix
+                    (mainServerNullableString
+                      (mainServerPageUriSpecificPart uri)
+                      "")
+                    "/")))
+            (if (string-blank? pdfId)
+              #f
+              (let ((preferences
+                      (mainServerPageActivityPreferences
+                        activity
+                        (mainServerPageModePrivate))))
+                (let ((count
+                        (mainServerPositiveCount
+                          (mainServerPagePreferenceInt
+                            preferences
+                            (mainServerPagePrefCount)
+                            (int32 1)))))
+                  (begin
+                    (mainServerPagePdfIdSet activity pdfId)
+                    (mainServerPageUriSet activity (nullable-some uri))
+                    (mainServerPagePdfNameSet
+                      activity
+                      (mainServerNullableString
+                        (mainServerPagePreferenceString
+                          preferences
+                          (mainServerPagePrefName)
+                          (nullable-none String))
+                        pdfId))
+                    (mainServerPagePdfSha1Set activity pdfId)
+                    (mainServerPageCountSet activity count)
+                    (mainServerPageIndexSet
+                      activity
+                      (mainServerRestoredPage page count))
+                    (mainServerPageRenderCurrent activity)
+                    #t))))))
+        (def (mainServerCatalogLabels
+               (pdfs : (List SfbServerPdf))) : (MutableList String)
+          (for/fold ((labels (mutable-list-empty String)))
+                    ((i (in-range (int32 0) (list-size pdfs))))
+            (let ((pdf (list-ref pdfs i)))
+              (begin
+                (mutable-list-add!
+                  labels
+                  (string-append
+                    (mainServerPagePdfLabel pdf)
+                    (string-append
+                      " ("
+                      (string-append
+                        (int32->string
+                          (mainServerPositiveCount
+                            (mainServerPagePdfPages pdf)))
+                        " pages)"))))
+                labels))))
+        (def (mainServerLoadPdf
+               (activity : MainActivity)
+               (pdf : SfbServerPdf)) : Unit
+          (let ((pdfId (mainServerPagePdfId pdf)))
+            (if (string-blank? pdfId)
+              (mainServerPageSetStatus activity "Server PDF missing id")
+              (begin
+                (mainServerPagePdfIdSet activity pdfId)
+                (mainServerPageUriSet
+                  activity
+                  (nullable-some
+                    (mainServerPageUriParse
+                      (string-append "sfbserver:" pdfId))))
+                (mainServerPagePdfNameSet
+                  activity
+                  (if (string-blank? (mainServerPagePdfLabel pdf))
+                    pdfId
+                    (mainServerPagePdfLabel pdf)))
+                (mainServerPagePdfSha1Set activity pdfId)
+                (mainServerPageIndexSet activity (int32 0))
+                (mainServerPageCountSet
+                  activity
+                  (mainServerPositiveCount
+                    (mainServerPagePdfPages pdf)))
+                (mainServerPagePersist activity #f)
+                (mainServerPageRenderCurrent activity)))))
+        (def (mainServerShowCatalog
+               (activity : MainActivity)
+               (catalog : SfbServerCatalog)) : Unit
+          (let ((pdfs (mainServerPageCatalogPdfs catalog)))
+            (if (= (list-size pdfs) (int32 0))
+              (mainServerPageSetStatus activity "Server returned no SSD PDFs")
+              (begin
+                (mainServerPageBuilderShow
+                  (mainServerPageBuilderNegative
+                    (mainServerPageBuilderItems
+                      (mainServerPageBuilderTitle
+                        (mainServerPageBuilder activity)
+                        "Server SSDs")
+                      (mainServerPageStringArray
+                        (mainServerCatalogLabels pdfs))
+                      (lambda ((unused : DialogInterface) (which : Int32))
+                        (mainServerLoadPdf activity (list-ref pdfs which))))
+                    "Cancel"
+                    (lambda ((unused : DialogInterface) (which : Int32))
+                      (begin))))
+                (mainServerPageSetStatus
+                  activity
+                  (string-append
+                    "Loaded "
+                    (string-append
+                      (int32->string (list-size pdfs))
+                      " server SSD PDFs")))))))
+        (def (mainServerOpenPdfs (activity : MainActivity)) : Unit
+          (begin
+            (mainServerPageSetStatus activity "Loading server SSD catalog...")
+            (mainServerPageRunBackground
+              "sfb-server-catalog"
+              (lambda ()
+                (try
+                  (let ((catalog
+                          (mainServerPageClientCatalog
+                            (mainServerPageClient activity))))
+                    (mainServerPageRunOnUi
+                      activity
+                      (lambda ()
+                        (mainServerShowCatalog activity catalog))))
+                  (catch (error : Exception)
+                    (mainServerPageRunOnUi
+                      activity
+                      (lambda ()
+                        (mainServerPageSetStatus
+                          activity
+                          (string-append
+                            "Server SSD catalog failed: "
+                            (mainServerPageErrorText error)))))))))))
+        (def (mainServerFindEndpointAsync
+               (activity : MainActivity)) : Unit
+          (begin
+            (mainServerPageSetStatus activity "Finding sfb-server...")
+            (mainServerPageRunBackground
+              "sfb-server-find"
+              (lambda ()
+                (try
+                  (let ((client (mainServerPageClient activity)))
+                    (begin
+                      (mainServerPageClientRefresh client)
+                      (mainServerPageRunOnUi
+                        activity
+                        (lambda ()
+                          (mainServerPageSetStatus
+                            activity
+                            (string-append
+                              "Server ready: "
+                              (mainServerPageClientEndpoint client)))))))
+                  (catch (error : Exception)
+                    (mainServerPageRunOnUi
+                      activity
+                      (lambda ()
+                        (mainServerPageSetStatus
+                          activity
+                          (string-append
+                            "Server discovery failed: "
+                            (mainServerPageErrorText error)))))))))))
+        (def (mainServerShowEndpointDialog
+               (activity : MainActivity)) : Unit
+          (let ((input (mainServerPageEditText activity)))
+            (begin
+              (mainServerPageEditTextSet
+                input
+                (mainServerPageEndpointLabel activity))
+              (mainServerPageBuilderShow
+                (mainServerPageBuilderNegative
+                  (mainServerPageBuilderPositive
+                    (mainServerPageBuilderView
+                      (mainServerPageBuilderMessage
+                        (mainServerPageBuilderTitle
+                          (mainServerPageBuilder activity)
+                          "Server URL")
+                        "HTTP is restricted to local/private hosts.")
+                      input)
+                    "Save"
+                    (lambda ((unused : DialogInterface) (which : Int32))
+                      (try
+                        (begin
+                          (mainServerPageSetEndpoint
+                            activity
+                            (mainServerPageEditableText
+                              (mainServerPageEditTextEditable input)))
+                          (mainServerFindEndpointAsync activity))
+                        (catch (error : Exception)
+                          (mainServerPageSetStatus
+                            activity
+                            (string-append
+                              "Server URL rejected: "
+                              (mainServerPageErrorText error)))))))
+                  "Cancel"
+                  (lambda ((unused : DialogInterface) (which : Int32))
+                    (begin))))
+              (begin))))
+        (def (mainServerNewSession
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (bitmap : Bitmap)) : SsdSession
+          (mainServerPageMakeSession
+            (mainServerPageSessionId
+              (mainServerPageUuidText (mainServerPageUuid)))
+            (mainServerPageSourceKey
+              pdfId
+              (+ pageIndex (int32 1))
+              (mainServerPageDpi))
+            name
+            (mainServerPageUriText uri)
+            (+ pageIndex (int32 1))
+            pageCount
+            (mainServerPageDpi)
+            (mainServerPageBitmapWidth bitmap)
+            (mainServerPageBitmapHeight bitmap)))
+        (def (mainServerTruthSession
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (bitmap : Bitmap)
+               (truth : (Nullable JSONObject))
+               (manifestArea : (Nullable FloatArray))) : (Nullable SsdSession)
+          (if (nullable-null? truth)
+            (nullable-none SsdSession)
+            (let ((session
+                    (mainServerNewSession
+                      pdfId name uri pageIndex pageCount bitmap)))
+              (begin
+                (mainServerPageSessionAreaSet session manifestArea)
+                (mainServerPageApplyTruth session (nullable-get truth))
+                (if (nullable-null? (mainServerPageSessionArea session))
+                  (mainServerPageSessionAreaSet session manifestArea)
+                  (begin))
+                (mainServerPagePrune session)
+                (if (> (list-size (mainServerPageSessionGroups session))
+                       (int32 0))
+                  (nullable-some session)
+                  (nullable-none SsdSession))))))
+        (def (mainServerRecognitionSession
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (bitmap : Bitmap)
+               (recognition : SfbServerRecognition)
+               (manifestArea : (Nullable FloatArray))) : SsdSession
+          (let ((session
+                  (mainServerNewSession
+                    pdfId name uri pageIndex pageCount bitmap)))
+            (begin
+              (mainServerPageSessionAreaSet
+                session
+                (if (nullable-null?
+                      (mainServerPageRecognitionArea recognition))
+                  manifestArea
+                  (mainServerPageRecognitionArea recognition)))
+              (mutable-list-add-all!
+                (mainServerPageSessionCells session)
+                (mainServerPageRecognitionCells recognition))
+              (mutable-list-add-all!
+                (mainServerPageSessionGroups session)
+                (mainServerPageRecognitionGroups recognition))
+              (mutable-list-add-all!
+                (mainServerPageSessionWords session)
+                (mainServerPageRecognitionWords recognition))
+              (mainServerPagePrune session)
+              session)))
+        (def (mainServerFetchTruth
+               (client : SfbServerClient)
+               (pdfId : String)
+               (page : Int32)) : (Nullable JSONObject)
+          (try
+            (mainServerPageTruthJson
+              (mainServerPageClientTruth
+                client pdfId page (mainServerPageDpi)))
+            (catch (error : Exception)
+              (nullable-none JSONObject))))
+        (def (mainServerDecodeSnapshot
+               (snapshot : SfbServerSnapshot)) : Bitmap
+          (let ((bytes (mainServerPageSnapshotBytes snapshot)))
+            (let ((bitmap
+                    (mainServerPageDecodeBitmap
+                      bytes
+                      (int32 0)
+                      (mainServerPageBytesSize bytes))))
+              (if (nullable-null? bitmap)
+                (throw
+                  (new IllegalStateException
+                    "Server snapshot was not a bitmap")
+                  Bitmap)
+                (nullable-get bitmap)))))
+        (type IllegalStateException)
+        (def (mainServerLoadStillCurrent
+               (activity : MainActivity)
+               (generation : Int32)
+               (uri : Uri)
+               (page : Int32)) : Bool
+          (mainServerPageCurrentLoad
+            generation
+            (mainServerPageLoadGeneration activity)
+            (mainServerPageUri activity)
+            uri
+            (mainServerPageIndex activity)
+            page))
+        (def (mainServerApplyRecognition
+               (activity : MainActivity)
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (bitmap : Bitmap)
+               (manifestArea : (Nullable FloatArray))
+               (generation : Int32)
+               (recognition : SfbServerRecognition)) : Unit
+          (if (mainServerLoadStillCurrent
+                activity generation uri pageIndex)
+            (let ((session
+                    (mainServerRecognitionSession
+                      pdfId name uri pageIndex pageCount bitmap
+                      recognition manifestArea)))
+              (begin
+                (mainServerPageShowSession
+                  activity
+                  (mainServerPageReviewView activity)
+                  session)
+                (mainServerPageReviewRefresh
+                  (mainServerPageReviewView activity))
+                (mainServerPageSetStatus
+                  activity
+                  (string-append
+                    "Server recognition applied: "
+                    (string-append
+                      (int32->string
+                        (list-size
+                          (mainServerPageSessionGroups session)))
+                      " groups")))))
+            (begin)))
+        (def (mainServerFetchRecognitionAsync
+               (activity : MainActivity)
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (bitmap : Bitmap)
+               (manifestArea : (Nullable FloatArray))
+               (generation : Int32)) : Unit
+          (mainServerPageRunBackground
+            "sfb-server-recognition"
+            (lambda ()
+              (try
+                (let ((recognition
+                        (mainServerPageClientRecognition
+                          (mainServerPageClient activity)
+                          pdfId
+                          (+ pageIndex (int32 1))
+                          (mainServerPageDpi))))
+                  (mainServerPageRunOnUi
+                    activity
+                    (lambda ()
+                      (mainServerApplyRecognition
+                        activity pdfId name uri pageIndex pageCount bitmap
+                        manifestArea generation recognition))))
+                (catch (error : Exception)
+                  (mainServerPageRunOnUi
+                    activity
+                    (lambda ()
+                      (if (mainServerLoadStillCurrent
+                            activity generation uri pageIndex)
+                        (mainServerPageSetStatus
+                          activity
+                          (string-append
+                            "Server recognition skipped: "
+                            (mainServerPageErrorText error)))
+                        (begin)))))))))
+        (def (mainServerShowLoadedPage
+               (activity : MainActivity)
+               (pdfId : String)
+               (name : String)
+               (uri : Uri)
+               (pageIndex : Int32)
+               (pageCount : Int32)
+               (generation : Int32)
+               (bitmap : Bitmap)
+               (manifestArea : (Nullable FloatArray))
+               (truthSession : (Nullable SsdSession))
+               (viewport : (Nullable ViewportState))) : Unit
+          (if (mainServerLoadStillCurrent
+                activity generation uri pageIndex)
+            (begin
+              (mainServerPageShowBitmap
+                activity
+                (mainServerPageReviewView activity)
+                bitmap)
+              (if (nullable-null? truthSession)
+                (begin)
+                (mainServerPageShowSession
+                  activity
+                  (mainServerPageReviewView activity)
+                  (nullable-get truthSession)))
+              (mainServerPageRestoreViewport
+                (mainServerPageReviewView activity)
+                viewport)
+              (mainServerPageSetStatus
+                activity
+                (string-append
+                  "Loaded server "
+                  (string-append
+                    name
+                    (string-append
+                      " page "
+                      (string-append
+                        (int32->string (+ pageIndex (int32 1)))
+                        (string-append
+                          "/"
+                          (string-append
+                            (int32->string pageCount)
+                            "; recognition running")))))))
+              (mainServerFetchRecognitionAsync
+                activity pdfId name uri pageIndex pageCount bitmap
+                manifestArea generation))
+            (begin)))
+        (def (mainServerRenderCurrentPage
+               (activity : MainActivity)
+               (uri : Uri)) : Unit
+          (let ((pdfId (mainServerPageCurrentPdfId activity))
+                (pageIndex (mainServerPageIndex activity))
+                (pageCount
+                  (mainServerPositiveCount
+                    (mainServerPageCount activity))))
+            (if (string-blank? pdfId)
+              (mainServerPageSetStatus activity "Server PDF id missing")
+              (let ((generation
+                      (mainServerPageAdvanceGeneration
+                        activity
+                        (mainServerPageLoadGeneration activity)))
+                    (name
+                      (if (string-blank? (mainServerPagePdfName activity))
+                        pdfId
+                        (mainServerPagePdfName activity)))
+                    (viewport
+                      (mainServerPageRestoredViewport
+                        activity uri pageIndex)))
+                (begin
+                  (mainServerPageSetStatus
+                    activity
+                    (string-append
+                      "Loading server page "
+                      (string-append
+                        (int32->string (+ pageIndex (int32 1)))
+                        (string-append "/" (int32->string pageCount)))))
+                  (mainServerPageRunBackground
+                    "sfb-server-page"
+                    (lambda ()
+                      (try
+                        (let ((client (mainServerPageClient activity)))
+                          (let ((manifest
+                                  (mainServerPageClientManifest
+                                    client pdfId
+                                    (+ pageIndex (int32 1))
+                                    (mainServerPageDpi))))
+                            (let ((bitmap
+                                    (mainServerDecodeSnapshot
+                                      (mainServerPageClientSnapshot
+                                        client pdfId
+                                        (+ pageIndex (int32 1))
+                                        (mainServerPageDpi))))
+                                  (truth
+                                    (mainServerFetchTruth
+                                      client pdfId
+                                      (+ pageIndex (int32 1)))))
+                              (let ((area
+                                      (mainServerPageManifestArea manifest)))
+                                (let ((session
+                                        (mainServerTruthSession
+                                          pdfId name uri pageIndex pageCount
+                                          bitmap truth area)))
+                                  (mainServerPageRunOnUi
+                                    activity
+                                    (lambda ()
+                                      (mainServerShowLoadedPage
+                                        activity pdfId name uri pageIndex
+                                        pageCount generation bitmap area
+                                        session viewport))))))))
+                        (catch (error : Exception)
+                          (mainServerPageRunOnUi
+                            activity
+                            (lambda ()
+                              (if (mainServerLoadStillCurrent
+                                    activity generation uri pageIndex)
+                                (mainServerPageSetStatus
+                                  activity
+                                  (string-append
+                                    "Server page failed: "
+                                    (mainServerPageErrorText error)))
+                                (begin)))))))))))))))))
diff --git a/templates/ssd-review-parts/server-ai.ss b/templates/ssd-review-parts/server-ai.ss
index 475200d..c0e840b 100644
--- a/templates/ssd-review-parts/server-ai.ss
+++ b/templates/ssd-review-parts/server-ai.ss
@@ -6,4 +6,5 @@
     (include-fragment "server-http.ss")
     (include-fragment "server-client.ss")
     (include-fragment "ai-report.ss")
-    (include-fragment "main-server-lifecycle.ss")))
+    (include-fragment "main-server-lifecycle.ss")
+    (include-fragment "main-server-pages.ss")))