constitution · epochs · watch · epoch 3

comparison

c_a4b313331fd6 (tommy-mor) vs c_7074f7a7bb57 (tommy-mor)

download prompt · raw event · cmp_f527472a931d60

council reasoning

~anthropic/claude-sonnet-latest · winner A · 8:2 · permalink

Side A adds substantive production infrastructure (Dockerfile, fly.toml, supervisord, entrypoint script) plus real bugfixes to JDBC URL parsing (handling postgresql:// scheme and query params), SSL config, and DB connection retry logic in Rust—concrete lasting deployment capability. Side B is a small, somewhat regressive UI tweak that removes pin-form functionality for unpinned items and only adds one CSS line, offering far less durable value.

~x-ai/grok-latest · winner A · 12:1 · permalink

A ships lasting production infrastructure (multi-stage Dockerfile, fly.toml, supervisord) plus real compatibility fixes: JDBC URL/SSL parsing, smaller pools, PORT env handling, and rustsorter DB connect retries. B only tweaks CSS (display:flex) and removes pin/vote-preview UI surface area—small polish with little enduring project value.

openai/gpt-chat-latest · winner A · 20:1 · permalink

Side A adds substantial production infrastructure and runtime robustness: a Fly.io deployment (Dockerfile, supervisor, entrypoint, fly.toml), support for managed PostgreSQL with SSL URL handling, PORT environment support, and retry logic for Rust database connections. Side B is a small UI cleanup, removing pin controls and a preview section while adding a single CSS display rule, with far less lasting architectural impact.

sides

A — c_a4b313331fd6 (tommy-mor)

message

[f6e0be88] add fly.io deployment with external postgres support

Ship a production docker image and fly config so sorter can run on a single fly machine against managed postgres, including ssl url parsing and db connection retries.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/borter/src/app/database.clj b/borter/src/app/database.clj
index 9a7251a94d3b237bc640541cdeb1845ef3669738..7d829634869ca4d66038d7ceeaf248fd488a566c 100644
--- a/borter/src/app/database.clj
+++ b/borter/src/app/database.clj
@@ -16,13 +16,21 @@
 
 (defn parse-jdbc [jdbc-url]
   (try
-    (drop 1 (re-matches
-             #"postgres://(.*):(.*)@(.*):(\d*)/(.*)"
-             jdbc-url))
+    (let [normalized (-> jdbc-url
+                         (clojure.string/replace #"^postgresql://" "postgres://")
+                         (clojure.string/replace #"\?.*$" ""))]
+      (drop 1 (re-matches
+               #"postgres://(.*):(.*)@(.*):(\d*)/(.*)"
+               normalized)))
     (catch Exception e
       (log/error "Failed to parse JDBC URL:" (.getMessage e))
       nil)))
 
+(defn ssl-config [jdbc-url]
+  (when (or (clojure.string/includes? (or jdbc-url "") "sslmode=require")
+            (= "require" (System/getenv "PGSSLMODE")))
+    {:ssl true :sslmode "require"}))
+
 (defn build-datasource []
   ;; Read DATABASE_URL *at runtime*
   (try
@@ -39,22 +47,23 @@
             (swap! db-state assoc :last-error "Invalid DATABASE_URL format")
             nil)
           (let [[username password host port database] parsed
-                cfg {:auto-commit        true
-                     :read-only          false
-                     :connection-timeout 30000
-                     :validation-timeout 5000
-                     :idle-timeout       600000
-                     :max-lifetime       1800000
-                     :minimum-idle       10
-                     :maximum-pool-size  10
-                     :pool-name          "db-pool"
-                     :register-mbeans    false
-                     :adapter            "postgresql"
-                     :username           username
-                     :password           password
-                     :server-name        host
-                     :port-number        (Integer/parseInt port)
-                     :database-name      database}]
+                cfg (merge {:auto-commit        true
+                            :read-only          false
+                            :connection-timeout 30000
+                            :validation-timeout 5000
+                            :idle-timeout       600000
+                            :max-lifetime       1800000
+                            :minimum-idle       2
+                            :maximum-pool-size  5
+                            :pool-name          "db-pool"
+                            :register-mbeans    false
+                            :adapter            "postgresql"
+                            :username           username
+                            :password           password
+                            :server-name        host
+                            :port-number        (Integer/parseInt port)
+                            :database-name      database}
+                           (ssl-config database-url))]
             (try
               (log/info "Attempting to create database connection pool")
               (make-datasource cfg)
diff --git a/borter/src/prod.clj b/borter/src/prod.clj
index 1b541a0628494a4e3bf6b339ebd543b8aaec25b8..746763bb14170b172d11060965a0abf511305891 100644
--- a/borter/src/prod.clj
+++ b/borter/src/prod.clj
@@ -81,23 +81,24 @@
 (defn -main
   "Run with `clj -M -m prod` or with optional port override {:port 3000}"
   [& {:keys [port] :or {port 8080}}]
-  (log/info "\n ______   _____   ______ _______ _______  ______\n |_____] |     | |_____/    |    |______ |_____/\n |_____] |_____| |    \\_    |    |______ |    \\_\n\n📦️" version "\n")
-  (log/info "starting borter" version "in" environment "environment")
-  (log/debug "server configuration:" electric-server-config)
-  (log/info "starting repl with config" (dissoc nrepl-config :auth))  ; don't log password
-  (reset! server-instance (nrepl-server/start-server nrepl-config))
-  (try
-    (db/run-migrations)
-    (log/info "database migrations completed successfully")
-    (catch Exception e
-      (log/error e "failed to run database migrations")))
-  (try
-    (http/start {:port port})
-    (log/info "http server started successfully on port" port)
-    (catch Exception e
-      (log/error e "failed to start http server")))
-  ;; Start the database health checker
-  (start-db-health-checker))
+  (let [port (or (some-> (System/getenv "PORT") Integer/parseInt)
+                 port)]
+    (log/info "\n ______   _____   ______ _______ _______  ______\n |_____] |     | |_____/    |    |______ |_____/\n |_____] |_____| |    \\_    |    |______ |    \\_\n\n📦️" version "\n")
+    (log/info "starting borter" version "in" environment "environment")
+    (log/debug "server configuration:" electric-server-config)
+    (log/info "starting repl with config" (dissoc nrepl-config :auth))  ; don't log password
+    (reset! server-instance (nrepl-server/start-server nrepl-config))
+    (try
+      (db/run-migrations)
+      (log/info "database migrations completed successfully")
+      (catch Exception e
+        (log/error e "failed to run database migrations")))
+    (try
+      (http/start {:port port})
+      (log/info "http server started successfully on port" port)
+      (catch Exception e
+        (log/error e "failed to start http server")))
+    (start-db-health-checker)))
 
 (defn -shutdown-hook []
   (log/info "Shutdown hook called, cleaning up resources")
diff --git a/docker/fly/Dockerfile b/docker/fly/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..47b3346f72a3cb4285cdf185003641729518f2d0
--- /dev/null
+++ b/docker/fly/Dockerfile
@@ -0,0 +1,50 @@
+# Stage 1: build frontend assets into borter/resources/public
+FROM node:20-bookworm-slim AS forter-build
+WORKDIR /app
+COPY forter/package.json forter/package-lock.json ./forter/
+RUN cd forter && npm ci
+COPY forter/ ./forter/
+COPY borter/resources/public ./borter/resources/public/
+RUN mkdir -p borter/resources/public && cd forter && ENVIRONMENT=staging npm run staging
+
+# Stage 2: build borter uberjar
+FROM zudsniper/clojure:openjdk-17-node AS borter-build
+WORKDIR /app/borter
+COPY borter/deps.edn borter/build.sh borter/version ./
+COPY borter/src ./src/
+COPY borter/src-build ./src-build/
+COPY borter/resources ./resources/
+COPY --from=forter-build /app/borter/resources/public ./resources/public/
+RUN clojure -P && VERSION=$(cat version) && clojure -X:build uberjar :version "\"$VERSION\""
+
+# Stage 3: build rustsorter
+FROM rust:1-bookworm AS rustsorter-build
+WORKDIR /app
+RUN apt-get update && apt-get install -y libpq-dev pkg-config && rm -rf /var/lib/apt/lists/*
+COPY rustsorter/Cargo.toml rustsorter/Cargo.lock ./
+COPY rustsorter/.sqlx ./.sqlx
+COPY rustsorter/src ./src
+ENV SQLX_OFFLINE=true
+RUN cargo build --release
+
+# Stage 4: runtime
+FROM eclipse-temurin:17-jre-jammy
+RUN apt-get update && apt-get install -y supervisor curl && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+COPY --from=borter-build /app/borter/target/sorter-*-standalone.jar /app/borter/sorter.jar
+COPY --from=rustsorter-build /app/target/release/rustsorter /app/rustsorter/rustsorter
+COPY docker/fly/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
+COPY docker/fly/entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh && mkdir -p /app/borter/logs
+
+ENV JAVA_OPTS="-Xmx256m -Xms64m"
+ENV ENVIRONMENT=staging
+ENV PORT=8080
+EXPOSE 8080
+
+HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
+  CMD curl -f http://127.0.0.1:${PORT}/api/health || exit 1
+
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/docker/fly/entrypoint.sh b/docker/fly/entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..cb210a8c2612370c8c8768029cc761ce9a591b1b
--- /dev/null
+++ b/docker/fly/entrypoint.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -euo pipefail
+
+if [ -z "${DATABASE_URL:-}" ]; then
+  echo "DATABASE_URL is required"
+  exit 1
+fi
+
+# normalize postgresql:// URLs and keep ssl params for downstream consumers
+export DATABASE_URL="${DATABASE_URL/postgresql:\/\//postgres:\/\/}"
+export PGSSLMODE="${PGSSLMODE:-require}"
+export PORT="${PORT:-8080}"
+export ENVIRONMENT="${ENVIRONMENT:-staging}"
+export REPL_PASSWORD="${REPL_PASSWORD:-fly-repl-password}"
+export HOSTNAME="${HOSTNAME:-localhost}"
+export BORTER_LOG_PATH="${BORTER_LOG_PATH:-/app/borter/logs}"
+export BORTER_LOG_LEVEL="${BORTER_LOG_LEVEL:-info}"
+export AUTO_LOGIN="${AUTO_LOGIN:-true}"
+export NO_RATE_LIMITS="${NO_RATE_LIMITS:-true}"
+export RUSTSORTER_URL="${RUSTSORTER_URL:-http://127.0.0.1:8081}"
+
+exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
diff --git a/docker/fly/supervisord.conf b/docker/fly/supervisord.conf
new file mode 100644
index 0000000000000000000000000000000000000000..5e2c67a1cffe379b0c53bd3ce2ec6b7184e789e5
--- /dev/null
+++ b/docker/fly/supervisord.conf
@@ -0,0 +1,28 @@
+[supervisord]
+nodaemon=true
+loglevel=info
+
+[program:rustsorter]
+command=/app/rustsorter/rustsorter
+directory=/app/rustsorter
+autostart=true
+autorestart=true
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+priority=10
+environment=DATABASE_URL="%(ENV_DATABASE_URL)s",ENVIRONMENT="%(ENV_ENVIRONMENT)s"
+
+[program:borter]
+command=java %(ENV_JAVA_OPTS)s -jar /app/borter/sorter.jar
+directory=/app/borter
+autostart=true
+autorestart=true
+startsecs=15
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+priority=20
+environment=DATABASE_URL="%(ENV_DATABASE_URL)s",ENVIRONMENT="%(ENV_ENVIRONMENT)s",PORT="%(ENV_PORT)s",REPL_PASSWORD="%(ENV_REPL_PASSWORD)s",HOSTNAME="%(ENV_HOSTNAME)s",BORTER_LOG_PATH="%(ENV_BORTER_LOG_PATH)s",BORTER_LOG_LEVEL="%(ENV_BORTER_LOG_LEVEL)s",AUTO_LOGIN="%(ENV_AUTO_LOGIN)s",NO_RATE_LIMITS="%(ENV_NO_RATE_LIMITS)s",RUSTSORTER_URL="%(ENV_RUSTSORTER_URL)s",PGSSLMODE="%(ENV_PGSSLMODE)s"
diff --git a/fly.toml b/fly.toml
new file mode 100644
index 0000000000000000000000000000000000000000..0ea6b263bdff021981dfda9aa6c48f9a4ba2f3a9
--- /dev/null
+++ b/fly.toml
@@ -0,0 +1,32 @@
+app = 'sorter-fly-app'
+primary_region = 'ewr'
+
+[build]
+  dockerfile = 'docker/fly/Dockerfile'
+
+[env]
+  ENVIRONMENT = 'staging'
+  PORT = '8080'
+  PGSSLMODE = 'require'
+  AUTO_LOGIN = 'true'
+  NO_RATE_LIMITS = 'true'
+
+[http_service]
+  internal_port = 8080
+  force_https = true
+  auto_stop_machines = 'stop'
+  auto_start_machines = true
+  min_machines_running = 1
+  processes = ['app']
+
+  [[http_service.checks]]
+    interval = '30s'
+    timeout = '10s'
+    grace_period = '180s'
+    method = 'GET'
+    path = '/index.html'
+
+[[vm]]
+  size = 'shared-cpu-1x'
+  memory = '512mb'
+  cpus = 1
diff --git a/rustsorter/src/main.rs b/rustsorter/src/main.rs
index 2dc534ca99566b170e948d11396baa95b91b5870..c4af5978509afe04a2ab940be0d3c96bbaef0571 100644
--- a/rustsorter/src/main.rs
+++ b/rustsorter/src/main.rs
@@ -8,7 +8,7 @@ pub struct AppState {
     pub db: sqlx::PgPool,
 }
 
-use std::{env, sync::Arc};
+use std::{env, sync::Arc, time::Duration};
 
 use actix_web::{App, HttpServer, middleware::Logger, web, HttpResponse, get};
 use dotenv::dotenv;
@@ -33,12 +33,21 @@ async fn main() -> Result<(), anyhow::Error> {
     // Retrieve the database URL from the environment
     let database_string = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
 
-    // Create the database pool
-    let pool = sqlx::postgres::PgPoolOptions::new()
-        .max_connections(5)
-        .connect(&database_string)
-        .await
-        .expect("Unable to connect to database");
+    // Create the database pool with retries for external databases
+    let pool = loop {
+        match sqlx::postgres::PgPoolOptions::new()
+            .max_connections(5)
+            .acquire_timeout(Duration::from_secs(10))
+            .connect(&database_string)
+            .await
+        {
+     

… preview truncated; 339 characters omitted

download full diff A

B — c_7074f7a7bb57 (tommy-mor)

message

[f7eb8b76] css

diff preview

diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 41f9e9c64a80a55a9d3ece2a6a1f592cf5e8d8dd..8fc6be1de8dd975f9547de615809222236be4b70 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -362,7 +362,6 @@ fn child_row_pin_or_vote(
     row_item: &ItemId,
     pinned_room_and_item: Option<&(String, ItemId)>,
     scope_content: &ContentState,
-    next_path: &str,
 ) -> maud::Markup {
     let pin_matches_scope = pinned_room_and_item
         .map(|(r, _)| r == nav.room_wire.as_str())
@@ -371,21 +370,9 @@ fn child_row_pin_or_vote(
         .filter(|_| pin_matches_scope)
         .map(|(_, i)| i);
 
-    let pin_rpc = template_json_compact(
-        &json!({
-            "action": "set_garden_pin",
-            "clear": false,
-            "room_wire": nav.room_wire.clone(),
-            "item_storage": row_item.as_str(),
-            "next": next_path,
-            "form_action": "/ui",
-        }),
-    )
-    .expect("child pin rpc json");
-
     html! {
-        span class="ont-garden-child-actions" data-garden-room=(nav.room_wire.as_str()) {
-            @if let Some(pi) = pinned_item {
+        @if let Some(pi) = pinned_item {
+            span class="ont-garden-child-actions" data-garden-room=(nav.room_wire.as_str()) {
                 @if pi == row_item {
                     span class="ont-garden-pinned-here" title="Pinned" aria-label="Pinned" { "📌" }
                 } @else {
@@ -400,11 +387,6 @@ fn child_row_pin_or_vote(
                         span class="ont-garden-vote-count" { (format!("{}", nv)) }
                     }
                 }
-            } @else {
-                form method="POST" action="/ui" data-navigate="full" class="ont-pin-form ont-garden-pin-form" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(pin_rpc);
-                    button type="submit" class="ont-garden-pin-ico" title="Pin" aria-label="Pin" { "📌" }
-                }
             }
         }
     }
@@ -1190,7 +1172,7 @@ async fn render_scope_view(
                                     @let item_url = item_href(r.item.as_str(), &nav);
                                     @let score_str = format!("{:.3}", r.score);
                                     li data-garden-item=(r.item.as_str()) {
-                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content, &next_for_pin))
+                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content))
                                         a class="item-link" href=(item_url) { code { (item_display_path(r.item.as_str())) } }
                                         span class="ont-rank-score" { (score_str) }
                                     }
@@ -1206,7 +1188,7 @@ async fn render_scope_view(
                         ul class="ont-group-list" {
                             @for name in &model.child_rankings.unranked_items {
                                 li data-garden-item=(name.as_str()) {
-                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content, &next_for_pin))
+                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content))
                                     @let href = item_href(name.as_str(), &nav);
                                     a class="item-link" href=(href) { code { (item_display_path(name.as_str())) } }
                                 }
@@ -1353,10 +1335,6 @@ async fn vote_compare_inner(
             div id="vote-edge-history-region" {
                 (edge_history)
             }
-            div class="vote-compare-preview-wrap" {
-                h3 { "your vote (after post)" }
-                div id="vote-compare-preview" class="vote-compare-preview" {}
-            }
             @if can_post {
                 form id="vote-compare-form" method="POST" action="/ui" {
                     input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json);
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index 0871e3126c656ba534306cdc3f405f802d10b589..6eb9222184a8795d67a5d09d41de08c8ac1b148f 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -638,6 +638,7 @@ body.view-ontology .ont-item-meta {
   flex-wrap: wrap;
   align-items: center;
   gap: 0.35rem;
+  display: flex;
 }
 body.view-ontology .ont-item-title {
   flex: 1 1 auto;

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.