constitution · epochs · watch · epoch 3

comparison

c_9f86dde118e5 (tommy-mor) vs c_a4b313331fd6 (tommy-mor)

download prompt · raw event · cmp_71445465c6a5b9

council reasoning

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

Side B delivers a concrete, functional deployment path (Dockerfile, fly.toml, supervisord, entrypoint, JDBC URL normalization/SSL handling, DB connection retry logic) that provides tangible new production capability. Side A is mostly a rename/refactor (navigate_panel->input_panel) plus removal of an unused vote_panel feature and minor CSS tweaks, which is smaller in scope and largely churn/cleanup rather than new lasting functionality.

~x-ai/grok-latest · winner B · 1:3 · permalink

B adds lasting production capability: multi-stage Docker image, fly.toml, supervisord/entrypoint, SSL-aware JDBC URL parsing, pool tuning, PORT env handling, and DB connect retries. A is mostly UI churn—deleting vote_panel/parser_render, folding navigate into input_panel, tiny CSS, and a local bb watch task—useful cleanup but not comparable operational substance.

openai/gpt-chat-latest · winner B · 4:1 · permalink

Side B adds production deployment infrastructure (Fly.io Dockerfile, supervisord, entrypoint, fly.toml) and improves runtime robustness by supporting both postgresql:// and postgres:// URLs, SSL configuration, PORT overrides, and retrying database connections for external Postgres. Side A mainly refactors the UI by moving the parser panel into the HTML module, removing the vote panel, adding a development watch task, and making small presentation tweaks, which are useful but have less enduring architectural impact.

sides

A — c_9f86dde118e5 (tommy-mor)

message

[ed042dc3] fixes

diff preview

diff --git a/bb.edn b/bb.edn
new file mode 100644
index 0000000000000000000000000000000000000000..1e8ec120921761a9a4f28f36578efb1567407cb3
--- /dev/null
+++ b/bb.edn
@@ -0,0 +1,19 @@
+{:paths ["."]
+ :deps {}
+ :tasks
+ {:requires ([babashka.process :as p]
+             [clojure.string :as str])
+
+  watch
+  {:doc "Hot-reload the server on source changes (requires cargo-watch)"
+   :task (do
+           (deref (p/process ["mkdir" "-p" "dev-data"] {:inherit true}))
+           (deref (p/process ["cargo" "watch"
+                              "-x" "run -p server"
+                              "-w" "server/"]
+                             {:inherit true
+                              :env (merge (into {} (System/getenv))
+                                          {"SLUG_DATA_DIR" "dev-data"
+                                           "SLUG_KEYS"     "dev:dev"
+                                           "PORT"          "8080"
+                                           "RUST_LOG"      "info"})})))}}}
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index e74e42e0f0ffc4224f5ef5421b486a5f35434999..d2024bd4582bcc8482b461b2ba4fedbd8bff7c66 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -6,9 +6,8 @@ use axum::{
 use std::collections::HashMap;
 
 use crate::{
-    html::{js_string_literal, ranking_panel, JsBuilder},
+    html::{input_panel, js_string_literal, ranking_panel, JsBuilder},
     parser::parse_reddit_url,
-    parser_render::navigate_panel,
     path_types::ItemId,
     reddit::ensure_partial_tree,
     state::{parse_item_param, AppState},
@@ -82,7 +81,7 @@ pub async fn post_ui_html(
                     .into_response()
             }
             Err(message) => {
-                let panel = navigate_panel(&query, Some(&message));
+                let panel = input_panel(&query, Some(&message));
                 JsBuilder::new()
                     .morph_selector("#parser-panel", panel)
                     .into_response()
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index a8353c6de0cd6d268219e552b7ca1ba4e3000585..322dffefeaa3b8560c1c2b2f70ba8e5a8c555022 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -8,7 +8,6 @@ use maud::{html, Markup, DOCTYPE};
 
 use crate::{
     form_template::template_json_compact,
-    parser_render::navigate_panel,
     path_types::ItemId,
     ranking::{top_bottom, RankedItem},
     reducer::{GroupState, NodeState},
@@ -224,49 +223,34 @@ pub fn ranking_panel(item: &ItemId, group: &GroupState) -> Markup {
     }
 }
 
-pub fn vote_panel(parent: &ItemId) -> Markup {
-    let parent_str = parent.as_str();
+pub fn input_panel(query: &str, error: Option<&str>) -> Markup {
     let rpc = template_json_compact(&serde_json::json!({
-        "action": "record_vote",
-        "a": {"$form": "item_a"},
-        "b": {"$form": "item_b"},
-        "ratio_left": 2,
-        "ratio_right": 1,
-        "scope": {"$form": "scope"}
+        "action": "parse_query",
+        "query": {"$form": "query"},
     }))
-    .expect("vote rpc json");
+    .expect("parse_query rpc template");
     html! {
-        section id="vote-panel" class="demo-panel" {
-            h2 { "Compare" }
-            p class="muted small" {
-                @if parent.is_root() {
-                    "Left item wins at 2:1. Votes append to the JSONL log and update rank centrality."
-                } @else {
-                    "Ranking children of "
-                    span class="scope-name" { (parent_str) }
-                    ". Left item wins at 2:1; each vote updates this ranking."
+        section id="parser-panel" class="demo-panel" {
+            form method="post" action="/ui" id="parser-form" {
+                textarea
+                    name="query"
+                    id="parser-input"
+                    rows="3"
+                    placeholder="https://reddit.com/r/rust or r/rust"
+                    autocomplete="off"
+                    spellcheck="false" {
+                    (query)
                 }
-            }
-            form method="post" action="/ui" id="vote-form" {
                 input type="hidden" name=(UI_RPC_FIELD) value=(rpc);
-                input type="hidden" name="scope" value=(parent_str);
-                div class="vote-fields" {
-                    label {
-                        "Left (wins) "
-                        input type="text" name="item_a" required placeholder="alpha" autocomplete="off";
-                    }
-                    label {
-                        "Right "
-                        input type="text" name="item_b" required placeholder="beta" autocomplete="off";
-                    }
-                }
-                button type="submit" class="btn-primary" { "Vote" }
+                button type="submit" class="btn-primary" { "Go" }
+            }
+            @if let Some(msg) = error {
+                p class="parser-error muted" { (msg) }
             }
         }
     }
 }
 
-
 async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup {
     let path = uri.path().to_string();
     state.views.increment(path.clone());
@@ -278,11 +262,10 @@ async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup {
     let group = &node.local_ranking;
 
     let body = html! {
-        h1 { "sorter2" }
+        h1 { "sorter" }
+        (input_panel("", None))
         (breadcrumb_path(&item))
-        (navigate_panel("", None))
         (entity_panel(node))
-        (vote_panel(&item))
         (ranking_panel(&item, group))
     };
     layout("sorter2", body, views)
diff --git a/server/src/lib.rs b/server/src/lib.rs
index cd56743192919cf4dcea539b3d8873a21fb7e72b..79b173f391a96ae5d0d96fd656e1e8d2dd070d09 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -4,7 +4,6 @@ pub mod events;
 pub mod form_template;
 pub mod html;
 pub mod parser;
-pub mod parser_render;
 pub mod path_types;
 pub mod ranking;
 pub mod reddit;
diff --git a/server/src/parser_render.rs b/server/src/parser_render.rs
deleted file mode 100644
index acf2e7403f4238291677ef0c79d5766302ea78cf..0000000000000000000000000000000000000000
--- a/server/src/parser_render.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use maud::{html, Markup};
-
-use crate::{
-    form_template::template_json_compact,
-    ui_action::UI_RPC_FIELD,
-};
-
-fn parse_query_rpc_template() -> String {
-    template_json_compact(&serde_json::json!({
-        "action": "parse_query",
-        "query": {"$form": "query"},
-    }))
-    .expect("parse_query rpc template")
-}
-
-/// Navigate panel: paste a Reddit URL and click Go.
-pub fn navigate_panel(query: &str, error: Option<&str>) -> Markup {
-    html! {
-        section id="parser-panel" class="demo-panel" {
-            h2 { "Navigate" }
-            p class="muted small" {
-                "Paste a Reddit URL or "
-                code { "r/subreddit" }
-                " path. Breadcrumb links drill down the tree; rankings apply to each node's children."
-            }
-            form method="post" action="/ui" id="parser-form" {
-                textarea
-                    name="query"
-                    id="parser-input"
-                    rows="3"
-                    placeholder="https://reddit.com/r/rust or r/rust"
-                    autocomplete="off"
-                    spellcheck="false" {
-                    (query)
-                }
-                input type="hidden" name=(UI_RPC_FIELD) value=(parse_query_rpc_template());
-                button type="submit" class="btn-primary" { "Go" }
-            }
-            @if let Some(msg) = error {
-                p class="parser-error muted" { (msg) }
-            }
-        }
-    }
-}
diff --git a/server/static/sorter.css b/server/static/sorter.css
index f5fb317a2c2a86d3dd00a639fff017fa3b8cc354..04f4fabdea3de7c3bc54805d0d3d9e0cea6eeefd 100644
--- a/server/static/sorter.css
+++ b/server/static/sorter.css
@@ -29,7 +29,7 @@ body {
 
 .view-meta {
   position: fixed;
-  top: 0.5rem;
+  top: 0.1rem;
   right: 0.5rem;
   font-size: 0.75rem;
 }
@@ -166,3 +166,7 @@ code {
   text-align: center;
   margin: 0.25rem 0;
 }
+
+h1 {
+  margin: 0;
+}
\ No newline at end of file
diff --git a/server/tests/integration_health.rs b/server/tests/integration_health.rs
index 1d816e008b9bf629967e3b3c786c41afefc284e3..244bdb69d680569b0074b31a9d7b98315b8ecaab 100644
--- a/server/tests/integration_health.rs
+++ b/server/tests/integration_health.rs
@@ -49,7 +49,6 @@ async fn home_has_main_panels() {
         .text()
         .await
         .unwrap();
-    assert!(html.contains("vote-panel"));
     assert!(html.contains("ranking-panel"));
     assert!(html.contains("parser-panel"));
     assert!(html.contains("__rpc__"));

download full diff A

B — 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 B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.