From 399e28d89bee1aeccf0065f6ec4146e84536b1a9 Mon Sep 17 00:00:00 2001 From: jimturner-adfa Date: Thu, 28 May 2026 12:50:29 -0700 Subject: [PATCH 1/3] WIP added code for dynamic bookshelf --- .../androidide/localWebServer/WebServer.kt | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt index fd6bc26f57..40d345fdd8 100644 --- a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt +++ b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt @@ -314,6 +314,7 @@ clientSocket and the catch block logic are updated accordingly. if (debugEnabled) log.debug("Found a pr/ path, '{}'.", path) return when (path) { + "pr/bs" -> handleBsEndpoint(writer, output) "pr/db" -> handleDbEndpoint(writer, output) "pr/pr" -> handlePrEndpoint(writer, output) "pr/ex" -> handleExEndpoint(writer, output) @@ -555,6 +556,31 @@ clientSocket and the catch block logic are updated accordingly. } } + private fun handleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream) { + if (debugEnabled) log.debug("Entering handleBsEndpoint().") + + var projectDatabase : SQLiteDatabase? = null + var outputStarted = false + + try { + projectDatabase = SQLiteDatabase.openDatabase(config.projectDatabasePath, + null, + SQLiteDatabase.OPEN_READONLY) + + outputStarted = realHandleBsEndpoint(writer, output, projectDatabase) + + } catch (e: Exception) { + log.error("Error handling /pr/bs endpoint: {}", e.message) + sendError(writer, output, 500, "Internal Server Error 6", "Error generating database table.", outputStarted) + + } finally { + projectDatabase?.close() + } + + if (debugEnabled) log.debug("Leaving handleBsEndpoint().") + } + + private fun handleExEndpoint(writer: PrintWriter, output: java.io.OutputStream) { val flag = if (experimentsEnabled) "{}" else "{display: none;}" @@ -618,6 +644,63 @@ second response. if (debugEnabled) log.debug("Leaving handlePrEndpoint().") } + private fun realHandleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream, projectDatabase: SQLiteDatabase) : Boolean { + if (debugEnabled) log.debug("Entering realHandleBsEndpoint().") + + val query = """ +SELECT id, + name, + DATETIME(create_at / 1000, 'unixepoch'), + DATETIME(last_modified / 1000, 'unixepoch'), + location, + template_name, + language +FROM recent_project_table +ORDER BY last_modified DESC""" + + var html = getTableHtml("Projects", "Projects") + """ + +Id +Name +Created +Modified   V +Directory +Template +Language +""" + + val cursor = projectDatabase.rawQuery(query, arrayOf()) + + try { + if (debugEnabled) log.debug("Retrieved {} rows.", cursor.count) + + while (cursor.moveToNext()) { + html += """ +${escapeHtml(cursor.getString(0) ?: "")} +${escapeHtml(cursor.getString(1) ?: "")} +${escapeHtml(cursor.getString(2) ?: "")} +${escapeHtml(cursor.getString(3) ?: "")} +${escapeHtml(cursor.getString(4) ?: "")} +${escapeHtml(cursor.getString(5) ?: "")} +${escapeHtml(cursor.getString(6) ?: "")} +""" + } + + html += "" + + } finally { + cursor.close() + } + + if (debugEnabled) log.debug("html is '{}'.", html) // May output a lot of stuff but better too much than too little. --DS, 23-Feb-2026 + + writeNormalToClient(writer, output, html) + + if (debugEnabled) log.debug("Leaving realHandlePrEndpoint().") + + return true + } + private fun realHandlePrEndpoint(writer: PrintWriter, output: java.io.OutputStream, projectDatabase: SQLiteDatabase) : Boolean { if (debugEnabled) log.debug("Entering realHandlePrEndpoint().") From ccfea349e3ffc5cafac54f722414cb69293eb178 Mon Sep 17 00:00:00 2001 From: jimturner-adfa Date: Fri, 29 May 2026 15:11:48 -0700 Subject: [PATCH 2/3] first working version of dynamic bookshelf --- .../androidide/localWebServer/WebServer.kt | 126 +++++++++++------- 1 file changed, 77 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt index 40d345fdd8..8e24fcb291 100644 --- a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt +++ b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt @@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentHashMap import io.pebbletemplates.pebble.template.PebbleTemplate import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import kotlinx.serialization.builtins.UByteArraySerializer +import okio.ByteString.Companion.toByteString data class ServerConfig( @@ -62,8 +64,9 @@ class WebServer(private val config: ServerConfig) { private val experimentsEnabled : Boolean = File(config.experimentsEnablePath).exists() // Frozen at startup. Restart server if needed. private val encodingHeader : String = "Accept-Encoding" private val brotliCompression : String = "br" - private val pebbleEngine = PebbleEngine.Builder().loader(StringLoader()).build() - private val templateCache = ConcurrentHashMap() + private val pebbleEngine = PebbleEngine.Builder().loader(StringLoader()).build() + private val templateCache = ConcurrentHashMap() + private var bookshelfTemplateId : Int = -1; private val contentChunkSize = 1024 * 1024 @@ -446,6 +449,7 @@ clientSocket and the catch block logic are updated accordingly. } else -> { val templateBlob = cursor.getBlob(0) + if (debugEnabled) log.debug("templateBlob = '${String(templateBlob)}'") pebbleEngine.getTemplate(templateBlob.toString(Charsets.UTF_8)) } } @@ -456,6 +460,10 @@ clientSocket and the catch block logic are updated accordingly. val mapper = ObjectMapper() val context: Map = mapper.readValue(dbContent.toString(Charsets.UTF_8), object : TypeReference>() {}) + /*******************DEBUGGING ONLY*******************/ + log.debug("context = ${context}") + /*******************DEBUGGING ONLY*******************/ + // Evaluate template with loaded data and return the output val sw = StringWriter() compiledTemplate.evaluate(sw, context) @@ -559,22 +567,15 @@ clientSocket and the catch block logic are updated accordingly. private fun handleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream) { if (debugEnabled) log.debug("Entering handleBsEndpoint().") - var projectDatabase : SQLiteDatabase? = null var outputStarted = false try { - projectDatabase = SQLiteDatabase.openDatabase(config.projectDatabasePath, - null, - SQLiteDatabase.OPEN_READONLY) - outputStarted = realHandleBsEndpoint(writer, output, projectDatabase) + outputStarted = realHandleBsEndpoint(writer, output) } catch (e: Exception) { log.error("Error handling /pr/bs endpoint: {}", e.message) - sendError(writer, output, 500, "Internal Server Error 6", "Error generating database table.", outputStarted) - - } finally { - projectDatabase?.close() + sendError(writer, output, 500, "Internal Server Error 6", "Error generating bookshelf HTML.", outputStarted) } if (debugEnabled) log.debug("Leaving handleBsEndpoint().") @@ -644,59 +645,86 @@ second response. if (debugEnabled) log.debug("Leaving handlePrEndpoint().") } - private fun realHandleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream, projectDatabase: SQLiteDatabase) : Boolean { + private fun realHandleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream) : Boolean { if (debugEnabled) log.debug("Entering realHandleBsEndpoint().") - val query = """ -SELECT id, - name, - DATETIME(create_at / 1000, 'unixepoch'), - DATETIME(last_modified / 1000, 'unixepoch'), - location, - template_name, - language -FROM recent_project_table -ORDER BY last_modified DESC""" + // Database fetch + val sql_query = +""" +SELECT '{"result" : [' || group_concat(Item) || ']}' FROM ( + SELECT + JSON_OBJECT( + 'category', IFNULL(BC.category, 'General'), + 'description', BC.description, + 'books', JSON_GROUP_ARRAY(JSON_OBJECT( + 'title', IFNULL(B.title, C.path), + 'description', B.description, + 'link', C.path) + ) + ) AS Item + FROM Content AS C, + Bookshelf AS B, + BookCategories AS BC + WHERE C.id = B.contentID + AND B.bookCategoryID = BC.id + GROUP BY BC.category + ORDER BY BC.category, + B.title +); +""".trimIndent() + + var cursor = database.rawQuery(sql_query, arrayOf()) + lateinit var jsonText : ByteArray - var html = getTableHtml("Projects", "Projects") + """ - -Id -Name -Created -Modified   V -Directory -Template -Language -""" + // Process database fetch + try { + if (cursor.count != 1) { + if (cursor.count == 0) + sendError(writer, output, 404, "Not Found") + else + sendError(writer, output, 500, "Corrupt database - ${cursor.count} bookshelf results found when one was expected.") + return false + } - val cursor = projectDatabase.rawQuery(query, arrayOf()) + //get the JSON from the bookshelf table + cursor.moveToFirst() + jsonText = cursor.getBlob(0) + if (debugEnabled) log.debug("json content = '${String(jsonText)}'.") + if (debugEnabled) log.debug("before fetch bookshelf template ID = '${bookshelfTemplateId}'") + + //Have we already fetched the template + if (bookshelfTemplateId == -1) { + cursor = database.rawQuery("SELECT id FROM Templates WHERE name = 'bookshelf'", arrayOf()) + + if (cursor.count != 1) { + if (cursor.count == 0) + sendError(writer, output, 404, "Not Found") + else + sendError(writer, output, 500, "Corrupt database - ${cursor.count} Bookshelf templates found when 1 was expected.") + return false + } - try { - if (debugEnabled) log.debug("Retrieved {} rows.", cursor.count) + cursor.moveToFirst() + bookshelfTemplateId = cursor.getInt(0); + if (debugEnabled) log.debug("after the fetch bookshelf template ID = '${bookshelfTemplateId}'") - while (cursor.moveToNext()) { - html += """ -${escapeHtml(cursor.getString(0) ?: "")} -${escapeHtml(cursor.getString(1) ?: "")} -${escapeHtml(cursor.getString(2) ?: "")} -${escapeHtml(cursor.getString(3) ?: "")} -${escapeHtml(cursor.getString(4) ?: "")} -${escapeHtml(cursor.getString(5) ?: "")} -${escapeHtml(cursor.getString(6) ?: "")} -""" } - html += "" + } catch (e: Exception) { + log.error("Error processing request: {}", e.message) + sendError(writer, output, 500, "Internal Server Error", e.message ?: "") } finally { cursor.close() } - if (debugEnabled) log.debug("html is '{}'.", html) // May output a lot of stuff but better too much than too little. --DS, 23-Feb-2026 + val result = instantiatePebbleTemplate(bookshelfTemplateId, jsonText, "/bookshelf", "application/json", "none") - writeNormalToClient(writer, output, html) + if (debugEnabled) log.debug("Bookshelf result is '{}'.", String(result)) - if (debugEnabled) log.debug("Leaving realHandlePrEndpoint().") + writeNormalToClient(writer, output, String(result)) + + if (debugEnabled) log.debug("Leaving realHandleBsEndpoint().") return true } From b32a684c2cbd44fcf8ac0fabab5e4d412aedf88a Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:31:16 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`ADF?= =?UTF-8?q?A-3802-Make-documentation-bookshelf-dynamic`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @davidschachterADFA. The following files were modified: * `app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt` --- .../androidide/localWebServer/WebServer.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt index 8e24fcb291..15d495d69f 100644 --- a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt +++ b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt @@ -246,6 +246,19 @@ clientSocket and the catch block logic are updated accordingly. return String(bytes, 0, len, Charsets.ISO_8859_1) } + /** + * Handles a single HTTP request on the given client socket and writes the corresponding HTTP response. + * + * Parses the request line and headers, enforces GET-only for normal routes, and routes high-priority + * "pr/" endpoints (including bookshelf, database table views, projects, and experiments). For other + * paths it reads content and metadata from the server's SQLite database, reassembles multi-fragment + * content, handles Brotli compression negotiation and decompression, optionally renders Pebble + * templates, and writes appropriate HTTP response headers and body. If a newer debug database is + * present on disk, swaps the server database to that file before serving the request. On errors + * sends an HTTP error response and logs the failure. + * + * @param clientSocket Connected client socket used for reading the request and writing the response. + */ private fun handleClient(clientSocket: Socket) { if (debugEnabled) log.debug("In handleClient(), socket is {}.", clientSocket) @@ -400,6 +413,17 @@ clientSocket and the catch block logic are updated accordingly. } } + /** + * Renders a Pebble template identified by `templateId` using the provided JSON data and returns the rendered output as bytes. + * + * @param templateId The database ID of the Pebble template to load and compile. + * @param dbContent JSON bytes that will be parsed and supplied as the template context. + * @param path The request/content path associated with this template (used for diagnostic/logging purposes). + * @param dbMimeType The MIME type of the stored content (used for diagnostic/logging purposes). + * @param compression The compression label of the stored content (e.g., "br", "none") (used for diagnostic/logging purposes). + * @return The rendered template encoded as UTF-8 bytes. + * @throws Exception If the template ID is not found, is duplicated in the database, or if template lookup/instantiation fails. + */ private fun instantiatePebbleTemplate(templateId: Int, dbContent: ByteArray, path: String, dbMimeType: String, compression: String): ByteArray { if (debugEnabled) log.debug("Processing template for templateId={}", templateId) @@ -471,6 +495,14 @@ clientSocket and the catch block logic are updated accordingly. } + /** + * Serve an HTML page showing the 20 most recent rows of the `LastChange` table. + * + * Queries the table schema to determine column names, selects the latest 20 rows + * ordered by `changeTime`, escapes cell values for HTML, assembles an HTML table, + * and writes a normal 200 HTML response to the client. On database or rendering + * errors a 500 error response is sent. All database cursors are closed before returning. + */ private fun handleDbEndpoint(writer: PrintWriter, output: java.io.OutputStream) { if (debugEnabled) log.debug("Entering handleDbEndpoint().") @@ -564,6 +596,15 @@ clientSocket and the catch block logic are updated accordingly. } } + /** + * Handles the /pr/bs endpoint by invoking the bookshelf generator and sending a 500 error if generation fails. + * + * Calls realHandleBsEndpoint to produce and write the response body; if an exception occurs, sends an HTTP 500 + * error using the reported output-start state so no additional headers/body are written after output has begun. + * + * @param writer PrintWriter used for writing textual HTTP response headers. + * @param output Raw OutputStream used for writing the response body bytes. + */ private fun handleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream) { if (debugEnabled) log.debug("Entering handleBsEndpoint().") @@ -582,6 +623,11 @@ clientSocket and the catch block logic are updated accordingly. } + /** + * Writes a small CSS response that shows or hides elements with the + * `.code_on_the_go_experiment` class depending on the server's + * `experimentsEnabled` flag. + */ private fun handleExEndpoint(writer: PrintWriter, output: java.io.OutputStream) { val flag = if (experimentsEnabled) "{}" else "{display: none;}" @@ -590,6 +636,12 @@ clientSocket and the catch block logic are updated accordingly. sendCSS(writer, output, ".code_on_the_go_experiment $flag") } + /** + * Handle the /pr/pr endpoint by opening the project database, delegating page generation to realHandlePrEndpoint, and sending an HTTP 500 error if generation fails. + * + * @param writer PrintWriter used to write response headers. + * @param output OutputStream used to write response body bytes. + */ private fun handlePrEndpoint(writer: PrintWriter, output: java.io.OutputStream) { if (debugEnabled) log.debug("Entering handlePrEndpoint().") @@ -645,6 +697,13 @@ second response. if (debugEnabled) log.debug("Leaving handlePrEndpoint().") } + /** + * Builds the Bookshelf content, renders it with the `bookshelf` template, and sends the resulting response to the client. + * + * @param writer PrintWriter for sending HTTP headers and control output. + * @param output OutputStream for writing the response body bytes. + * @return `true` if the templated response was written to the client, `false` if an error response was sent or no output was produced. + */ private fun realHandleBsEndpoint(writer: PrintWriter, output: java.io.OutputStream) : Boolean { if (debugEnabled) log.debug("Entering realHandleBsEndpoint().") @@ -729,6 +788,14 @@ SELECT '{"result" : [' || group_concat(Item) || ']}' FROM ( return true } + /** + * Builds an HTML table of recent projects from the provided project database and writes it to the client. + * + * @param writer PrintWriter used for writing HTTP response headers. + * @param output OutputStream used for writing the HTTP response body. + * @param projectDatabase Read-only SQLiteDatabase containing the `recent_project_table`. + * @return `true` if an HTML response was written to the client. + */ private fun realHandlePrEndpoint(writer: PrintWriter, output: java.io.OutputStream, projectDatabase: SQLiteDatabase) : Boolean { if (debugEnabled) log.debug("Entering realHandlePrEndpoint().")