{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[ed042dc3] fixes\n\nSide A — unified diff (full patch):\ndiff --git a/bb.edn b/bb.edn\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..1e8ec120921761a9a4f28f36578efb1567407cb3\n--- /dev/null\n+++ b/bb.edn\n@@ -0,0 +1,19 @@\n+{:paths [\".\"]\n+ :deps {}\n+ :tasks\n+ {:requires ([babashka.process :as p]\n+ [clojure.string :as str])\n+\n+ watch\n+ {:doc \"Hot-reload the server on source changes (requires cargo-watch)\"\n+ :task (do\n+ (deref (p/process [\"mkdir\" \"-p\" \"dev-data\"] {:inherit true}))\n+ (deref (p/process [\"cargo\" \"watch\"\n+ \"-x\" \"run -p server\"\n+ \"-w\" \"server/\"]\n+ {:inherit true\n+ :env (merge (into {} (System/getenv))\n+ {\"SLUG_DATA_DIR\" \"dev-data\"\n+ \"SLUG_KEYS\" \"dev:dev\"\n+ \"PORT\" \"8080\"\n+ \"RUST_LOG\" \"info\"})})))}}}\ndiff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs\nindex e74e42e0f0ffc4224f5ef5421b486a5f35434999..d2024bd4582bcc8482b461b2ba4fedbd8bff7c66 100644\n--- a/server/src/api/ui_html.rs\n+++ b/server/src/api/ui_html.rs\n@@ -6,9 +6,8 @@ use axum::{\n use std::collections::HashMap;\n \n use crate::{\n- html::{js_string_literal, ranking_panel, JsBuilder},\n+ html::{input_panel, js_string_literal, ranking_panel, JsBuilder},\n parser::parse_reddit_url,\n- parser_render::navigate_panel,\n path_types::ItemId,\n reddit::ensure_partial_tree,\n state::{parse_item_param, AppState},\n@@ -82,7 +81,7 @@ pub async fn post_ui_html(\n .into_response()\n }\n Err(message) => {\n- let panel = navigate_panel(&query, Some(&message));\n+ let panel = input_panel(&query, Some(&message));\n JsBuilder::new()\n .morph_selector(\"#parser-panel\", panel)\n .into_response()\ndiff --git a/server/src/html/mod.rs b/server/src/html/mod.rs\nindex a8353c6de0cd6d268219e552b7ca1ba4e3000585..322dffefeaa3b8560c1c2b2f70ba8e5a8c555022 100644\n--- a/server/src/html/mod.rs\n+++ b/server/src/html/mod.rs\n@@ -8,7 +8,6 @@ use maud::{html, Markup, DOCTYPE};\n \n use crate::{\n form_template::template_json_compact,\n- parser_render::navigate_panel,\n path_types::ItemId,\n ranking::{top_bottom, RankedItem},\n reducer::{GroupState, NodeState},\n@@ -224,49 +223,34 @@ pub fn ranking_panel(item: &ItemId, group: &GroupState) -> Markup {\n }\n }\n \n-pub fn vote_panel(parent: &ItemId) -> Markup {\n- let parent_str = parent.as_str();\n+pub fn input_panel(query: &str, error: Option<&str>) -> Markup {\n let rpc = template_json_compact(&serde_json::json!({\n- \"action\": \"record_vote\",\n- \"a\": {\"$form\": \"item_a\"},\n- \"b\": {\"$form\": \"item_b\"},\n- \"ratio_left\": 2,\n- \"ratio_right\": 1,\n- \"scope\": {\"$form\": \"scope\"}\n+ \"action\": \"parse_query\",\n+ \"query\": {\"$form\": \"query\"},\n }))\n- .expect(\"vote rpc json\");\n+ .expect(\"parse_query rpc template\");\n html! {\n- section id=\"vote-panel\" class=\"demo-panel\" {\n- h2 { \"Compare\" }\n- p class=\"muted small\" {\n- @if parent.is_root() {\n- \"Left item wins at 2:1. Votes append to the JSONL log and update rank centrality.\"\n- } @else {\n- \"Ranking children of \"\n- span class=\"scope-name\" { (parent_str) }\n- \". Left item wins at 2:1; each vote updates this ranking.\"\n+ section id=\"parser-panel\" class=\"demo-panel\" {\n+ form method=\"post\" action=\"/ui\" id=\"parser-form\" {\n+ textarea\n+ name=\"query\"\n+ id=\"parser-input\"\n+ rows=\"3\"\n+ placeholder=\"https://reddit.com/r/rust or r/rust\"\n+ autocomplete=\"off\"\n+ spellcheck=\"false\" {\n+ (query)\n }\n- }\n- form method=\"post\" action=\"/ui\" id=\"vote-form\" {\n input type=\"hidden\" name=(UI_RPC_FIELD) value=(rpc);\n- input type=\"hidden\" name=\"scope\" value=(parent_str);\n- div class=\"vote-fields\" {\n- label {\n- \"Left (wins) \"\n- input type=\"text\" name=\"item_a\" required placeholder=\"alpha\" autocomplete=\"off\";\n- }\n- label {\n- \"Right \"\n- input type=\"text\" name=\"item_b\" required placeholder=\"beta\" autocomplete=\"off\";\n- }\n- }\n- button type=\"submit\" class=\"btn-primary\" { \"Vote\" }\n+ button type=\"submit\" class=\"btn-primary\" { \"Go\" }\n+ }\n+ @if let Some(msg) = error {\n+ p class=\"parser-error muted\" { (msg) }\n }\n }\n }\n }\n \n-\n async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup {\n let path = uri.path().to_string();\n state.views.increment(path.clone());\n@@ -278,11 +262,10 @@ async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup {\n let group = &node.local_ranking;\n \n let body = html! {\n- h1 { \"sorter2\" }\n+ h1 { \"sorter\" }\n+ (input_panel(\"\", None))\n (breadcrumb_path(&item))\n- (navigate_panel(\"\", None))\n (entity_panel(node))\n- (vote_panel(&item))\n (ranking_panel(&item, group))\n };\n layout(\"sorter2\", body, views)\ndiff --git a/server/src/lib.rs b/server/src/lib.rs\nindex cd56743192919cf4dcea539b3d8873a21fb7e72b..79b173f391a96ae5d0d96fd656e1e8d2dd070d09 100644\n--- a/server/src/lib.rs\n+++ b/server/src/lib.rs\n@@ -4,7 +4,6 @@ pub mod events;\n pub mod form_template;\n pub mod html;\n pub mod parser;\n-pub mod parser_render;\n pub mod path_types;\n pub mod ranking;\n pub mod reddit;\ndiff --git a/server/src/parser_render.rs b/server/src/parser_render.rs\ndeleted file mode 100644\nindex acf2e7403f4238291677ef0c79d5766302ea78cf..0000000000000000000000000000000000000000\n--- a/server/src/parser_render.rs\n+++ /dev/null\n@@ -1,44 +0,0 @@\n-use maud::{html, Markup};\n-\n-use crate::{\n- form_template::template_json_compact,\n- ui_action::UI_RPC_FIELD,\n-};\n-\n-fn parse_query_rpc_template() -> String {\n- template_json_compact(&serde_json::json!({\n- \"action\": \"parse_query\",\n- \"query\": {\"$form\": \"query\"},\n- }))\n- .expect(\"parse_query rpc template\")\n-}\n-\n-/// Navigate panel: paste a Reddit URL and click Go.\n-pub fn navigate_panel(query: &str, error: Option<&str>) -> Markup {\n- html! {\n- section id=\"parser-panel\" class=\"demo-panel\" {\n- h2 { \"Navigate\" }\n- p class=\"muted small\" {\n- \"Paste a Reddit URL or \"\n- code { \"r/subreddit\" }\n- \" path. Breadcrumb links drill down the tree; rankings apply to each node's children.\"\n- }\n- form method=\"post\" action=\"/ui\" id=\"parser-form\" {\n- textarea\n- name=\"query\"\n- id=\"parser-input\"\n- rows=\"3\"\n- placeholder=\"https://reddit.com/r/rust or r/rust\"\n- autocomplete=\"off\"\n- spellcheck=\"false\" {\n- (query)\n- }\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(parse_query_rpc_template());\n- button type=\"submit\" class=\"btn-primary\" { \"Go\" }\n- }\n- @if let Some(msg) = error {\n- p class=\"parser-error muted\" { (msg) }\n- }\n- }\n- }\n-}\ndiff --git a/server/static/sorter.css b/server/static/sorter.css\nindex f5fb317a2c2a86d3dd00a639fff017fa3b8cc354..04f4fabdea3de7c3bc54805d0d3d9e0cea6eeefd 100644\n--- a/server/static/sorter.css\n+++ b/server/static/sorter.css\n@@ -29,7 +29,7 @@ body {\n \n .view-meta {\n position: fixed;\n- top: 0.5rem;\n+ top: 0.1rem;\n right: 0.5rem;\n font-size: 0.75rem;\n }\n@@ -166,3 +166,7 @@ code {\n text-align: center;\n margin: 0.25rem 0;\n }\n+\n+h1 {\n+ margin: 0;\n+}\n\\ No newline at end of file\ndiff --git a/server/tests/integration_health.rs b/server/tests/integration_health.rs\nindex 1d816e008b9bf629967e3b3c786c41afefc284e3..244bdb69d680569b0074b31a9d7b98315b8ecaab 100644\n--- a/server/tests/integration_health.rs\n+++ b/server/tests/integration_health.rs\n@@ -49,7 +49,6 @@ async fn home_has_main_panels() {\n .text()\n .await\n .unwrap();\n- assert!(html.contains(\"vote-panel\"));\n assert!(html.contains(\"ranking-panel\"));\n assert!(html.contains(\"parser-panel\"));\n assert!(html.contains(\"__rpc__\"));\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[f6e0be88] add fly.io deployment with external postgres support\n\nShip 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.\n\nCo-authored-by: Cursor \n\nSide B — unified diff (full patch):\ndiff --git a/borter/src/app/database.clj b/borter/src/app/database.clj\nindex 9a7251a94d3b237bc640541cdeb1845ef3669738..7d829634869ca4d66038d7ceeaf248fd488a566c 100644\n--- a/borter/src/app/database.clj\n+++ b/borter/src/app/database.clj\n@@ -16,13 +16,21 @@\n \n (defn parse-jdbc [jdbc-url]\n (try\n- (drop 1 (re-matches\n- #\"postgres://(.*):(.*)@(.*):(\\d*)/(.*)\"\n- jdbc-url))\n+ (let [normalized (-> jdbc-url\n+ (clojure.string/replace #\"^postgresql://\" \"postgres://\")\n+ (clojure.string/replace #\"\\?.*$\" \"\"))]\n+ (drop 1 (re-matches\n+ #\"postgres://(.*):(.*)@(.*):(\\d*)/(.*)\"\n+ normalized)))\n (catch Exception e\n (log/error \"Failed to parse JDBC URL:\" (.getMessage e))\n nil)))\n \n+(defn ssl-config [jdbc-url]\n+ (when (or (clojure.string/includes? (or jdbc-url \"\") \"sslmode=require\")\n+ (= \"require\" (System/getenv \"PGSSLMODE\")))\n+ {:ssl true :sslmode \"require\"}))\n+\n (defn build-datasource []\n ;; Read DATABASE_URL *at runtime*\n (try\n@@ -39,22 +47,23 @@\n (swap! db-state assoc :last-error \"Invalid DATABASE_URL format\")\n nil)\n (let [[username password host port database] parsed\n- cfg {:auto-commit true\n- :read-only false\n- :connection-timeout 30000\n- :validation-timeout 5000\n- :idle-timeout 600000\n- :max-lifetime 1800000\n- :minimum-idle 10\n- :maximum-pool-size 10\n- :pool-name \"db-pool\"\n- :register-mbeans false\n- :adapter \"postgresql\"\n- :username username\n- :password password\n- :server-name host\n- :port-number (Integer/parseInt port)\n- :database-name database}]\n+ cfg (merge {:auto-commit true\n+ :read-only false\n+ :connection-timeout 30000\n+ :validation-timeout 5000\n+ :idle-timeout 600000\n+ :max-lifetime 1800000\n+ :minimum-idle 2\n+ :maximum-pool-size 5\n+ :pool-name \"db-pool\"\n+ :register-mbeans false\n+ :adapter \"postgresql\"\n+ :username username\n+ :password password\n+ :server-name host\n+ :port-number (Integer/parseInt port)\n+ :database-name database}\n+ (ssl-config database-url))]\n (try\n (log/info \"Attempting to create database connection pool\")\n (make-datasource cfg)\ndiff --git a/borter/src/prod.clj b/borter/src/prod.clj\nindex 1b541a0628494a4e3bf6b339ebd543b8aaec25b8..746763bb14170b172d11060965a0abf511305891 100644\n--- a/borter/src/prod.clj\n+++ b/borter/src/prod.clj\n@@ -81,23 +81,24 @@\n (defn -main\n \"Run with `clj -M -m prod` or with optional port override {:port 3000}\"\n [& {:keys [port] :or {port 8080}}]\n- (log/info \"\\n ______ _____ ______ _______ _______ ______\\n |_____] | | |_____/ | |______ |_____/\\n |_____] |_____| | \\\\_ | |______ | \\\\_\\n\\n📦️\" version \"\\n\")\n- (log/info \"starting borter\" version \"in\" environment \"environment\")\n- (log/debug \"server configuration:\" electric-server-config)\n- (log/info \"starting repl with config\" (dissoc nrepl-config :auth)) ; don't log password\n- (reset! server-instance (nrepl-server/start-server nrepl-config))\n- (try\n- (db/run-migrations)\n- (log/info \"database migrations completed successfully\")\n- (catch Exception e\n- (log/error e \"failed to run database migrations\")))\n- (try\n- (http/start {:port port})\n- (log/info \"http server started successfully on port\" port)\n- (catch Exception e\n- (log/error e \"failed to start http server\")))\n- ;; Start the database health checker\n- (start-db-health-checker))\n+ (let [port (or (some-> (System/getenv \"PORT\") Integer/parseInt)\n+ port)]\n+ (log/info \"\\n ______ _____ ______ _______ _______ ______\\n |_____] | | |_____/ | |______ |_____/\\n |_____] |_____| | \\\\_ | |______ | \\\\_\\n\\n📦️\" version \"\\n\")\n+ (log/info \"starting borter\" version \"in\" environment \"environment\")\n+ (log/debug \"server configuration:\" electric-server-config)\n+ (log/info \"starting repl with config\" (dissoc nrepl-config :auth)) ; don't log password\n+ (reset! server-instance (nrepl-server/start-server nrepl-config))\n+ (try\n+ (db/run-migrations)\n+ (log/info \"database migrations completed successfully\")\n+ (catch Exception e\n+ (log/error e \"failed to run database migrations\")))\n+ (try\n+ (http/start {:port port})\n+ (log/info \"http server started successfully on port\" port)\n+ (catch Exception e\n+ (log/error e \"failed to start http server\")))\n+ (start-db-health-checker)))\n \n (defn -shutdown-hook []\n (log/info \"Shutdown hook called, cleaning up resources\")\ndiff --git a/docker/fly/Dockerfile b/docker/fly/Dockerfile\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..47b3346f72a3cb4285cdf185003641729518f2d0\n--- /dev/null\n+++ b/docker/fly/Dockerfile\n@@ -0,0 +1,50 @@\n+# Stage 1: build frontend assets into borter/resources/public\n+FROM node:20-bookworm-slim AS forter-build\n+WORKDIR /app\n+COPY forter/package.json forter/package-lock.json ./forter/\n+RUN cd forter && npm ci\n+COPY forter/ ./forter/\n+COPY borter/resources/public ./borter/resources/public/\n+RUN mkdir -p borter/resources/public && cd forter && ENVIRONMENT=staging npm run staging\n+\n+# Stage 2: build borter uberjar\n+FROM zudsniper/clojure:openjdk-17-node AS borter-build\n+WORKDIR /app/borter\n+COPY borter/deps.edn borter/build.sh borter/version ./\n+COPY borter/src ./src/\n+COPY borter/src-build ./src-build/\n+COPY borter/resources ./resources/\n+COPY --from=forter-build /app/borter/resources/public ./resources/public/\n+RUN clojure -P && VERSION=$(cat version) && clojure -X:build uberjar :version \"\\\"$VERSION\\\"\"\n+\n+# Stage 3: build rustsorter\n+FROM rust:1-bookworm AS rustsorter-build\n+WORKDIR /app\n+RUN apt-get update && apt-get install -y libpq-dev pkg-config && rm -rf /var/lib/apt/lists/*\n+COPY rustsorter/Cargo.toml rustsorter/Cargo.lock ./\n+COPY rustsorter/.sqlx ./.sqlx\n+COPY rustsorter/src ./src\n+ENV SQLX_OFFLINE=true\n+RUN cargo build --release\n+\n+# Stage 4: runtime\n+FROM eclipse-temurin:17-jre-jammy\n+RUN apt-get update && apt-get install -y supervisor curl && rm -rf /var/lib/apt/lists/*\n+\n+WORKDIR /app\n+\n+COPY --from=borter-build /app/borter/target/sorter-*-standalone.jar /app/borter/sorter.jar\n+COPY --from=rustsorter-build /app/target/release/rustsorter /app/rustsorter/rustsorter\n+COPY docker/fly/supervisord.conf /etc/supervisor/conf.d/supervisord.conf\n+COPY docker/fly/entrypoint.sh /entrypoint.sh\n+RUN chmod +x /entrypoint.sh && mkdir -p /app/borter/logs\n+\n+ENV JAVA_OPTS=\"-Xmx256m -Xms64m\"\n+ENV ENVIRONMENT=staging\n+ENV PORT=8080\n+EXPOSE 8080\n+\n+HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \\\n+ CMD curl -f http://127.0.0.1:${PORT}/api/health || exit 1\n+\n+ENTRYPOINT [\"/entrypoint.sh\"]\ndiff --git a/docker/fly/entrypoint.sh b/docker/fly/entrypoint.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..cb210a8c2612370c8c8768029cc761ce9a591b1b\n--- /dev/null\n+++ b/docker/fly/entrypoint.sh\n@@ -0,0 +1,22 @@\n+#!/bin/bash\n+set -euo pipefail\n+\n+if [ -z \"${DATABASE_URL:-}\" ]; then\n+ echo \"DATABASE_URL is required\"\n+ exit 1\n+fi\n+\n+# normalize postgresql:// URLs and keep ssl params for downstream consumers\n+export DATABASE_URL=\"${DATABASE_URL/postgresql:\\/\\//postgres:\\/\\/}\"\n+export PGSSLMODE=\"${PGSSLMODE:-require}\"\n+export PORT=\"${PORT:-8080}\"\n+export ENVIRONMENT=\"${ENVIRONMENT:-staging}\"\n+export REPL_PASSWORD=\"${REPL_PASSWORD:-fly-repl-password}\"\n+export HOSTNAME=\"${HOSTNAME:-localhost}\"\n+export BORTER_LOG_PATH=\"${BORTER_LOG_PATH:-/app/borter/logs}\"\n+export BORTER_LOG_LEVEL=\"${BORTER_LOG_LEVEL:-info}\"\n+export AUTO_LOGIN=\"${AUTO_LOGIN:-true}\"\n+export NO_RATE_LIMITS=\"${NO_RATE_LIMITS:-true}\"\n+export RUSTSORTER_URL=\"${RUSTSORTER_URL:-http://127.0.0.1:8081}\"\n+\n+exec supervisord -c /etc/supervisor/conf.d/supervisord.conf\ndiff --git a/docker/fly/supervisord.conf b/docker/fly/supervisord.conf\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..5e2c67a1cffe379b0c53bd3ce2ec6b7184e789e5\n--- /dev/null\n+++ b/docker/fly/supervisord.conf\n@@ -0,0 +1,28 @@\n+[supervisord]\n+nodaemon=true\n+loglevel=info\n+\n+[program:rustsorter]\n+command=/app/rustsorter/rustsorter\n+directory=/app/rustsorter\n+autostart=true\n+autorestart=true\n+stdout_logfile=/dev/stdout\n+stdout_logfile_maxbytes=0\n+stderr_logfile=/dev/stderr\n+stderr_logfile_maxbytes=0\n+priority=10\n+environment=DATABASE_URL=\"%(ENV_DATABASE_URL)s\",ENVIRONMENT=\"%(ENV_ENVIRONMENT)s\"\n+\n+[program:borter]\n+command=java %(ENV_JAVA_OPTS)s -jar /app/borter/sorter.jar\n+directory=/app/borter\n+autostart=true\n+autorestart=true\n+startsecs=15\n+stdout_logfile=/dev/stdout\n+stdout_logfile_maxbytes=0\n+stderr_logfile=/dev/stderr\n+stderr_logfile_maxbytes=0\n+priority=20\n+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\"\ndiff --git a/fly.toml b/fly.toml\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..0ea6b263bdff021981dfda9aa6c48f9a4ba2f3a9\n--- /dev/null\n+++ b/fly.toml\n@@ -0,0 +1,32 @@\n+app = 'sorter-fly-app'\n+primary_region = 'ewr'\n+\n+[build]\n+ dockerfile = 'docker/fly/Dockerfile'\n+\n+[env]\n+ ENVIRONMENT = 'staging'\n+ PORT = '8080'\n+ PGSSLMODE = 'require'\n+ AUTO_LOGIN = 'true'\n+ NO_RATE_LIMITS = 'true'\n+\n+[http_service]\n+ internal_port = 8080\n+ force_https = true\n+ auto_stop_machines = 'stop'\n+ auto_start_machines = true\n+ min_machines_running = 1\n+ processes = ['app']\n+\n+ [[http_service.checks]]\n+ interval = '30s'\n+ timeout = '10s'\n+ grace_period = '180s'\n+ method = 'GET'\n+ path = '/index.html'\n+\n+[[vm]]\n+ size = 'shared-cpu-1x'\n+ memory = '512mb'\n+ cpus = 1\ndiff --git a/rustsorter/src/main.rs b/rustsorter/src/main.rs\nindex 2dc534ca99566b170e948d11396baa95b91b5870..c4af5978509afe04a2ab940be0d3c96bbaef0571 100644\n--- a/rustsorter/src/main.rs\n+++ b/rustsorter/src/main.rs\n@@ -8,7 +8,7 @@ pub struct AppState {\n pub db: sqlx::PgPool,\n }\n \n-use std::{env, sync::Arc};\n+use std::{env, sync::Arc, time::Duration};\n \n use actix_web::{App, HttpServer, middleware::Logger, web, HttpResponse, get};\n use dotenv::dotenv;\n@@ -33,12 +33,21 @@ async fn main() -> Result<(), anyhow::Error> {\n // Retrieve the database URL from the environment\n let database_string = env::var(\"DATABASE_URL\").expect(\"DATABASE_URL must be set\");\n \n- // Create the database pool\n- let pool = sqlx::postgres::PgPoolOptions::new()\n- .max_connections(5)\n- .connect(&database_string)\n- .await\n- .expect(\"Unable to connect to database\");\n+ // Create the database pool with retries for external databases\n+ let pool = loop {\n+ match sqlx::postgres::PgPoolOptions::new()\n+ .max_connections(5)\n+ .acquire_timeout(Duration::from_secs(10))\n+ .connect(&database_string)\n+ .await\n+ {\n+ Ok(pool) => break pool,\n+ Err(e) => {\n+ eprintln!(\"unable to connect to database: {e}, retrying in 5s...\");\n+ tokio::time::sleep(Duration::from_secs(5)).await;\n+ }\n+ }\n+ };\n \n // Shared application state\n let app_state = Arc::new(AppState { db: pool.clone() });\n","role":"user"}],"model":"openai/gpt-chat-latest"}