constitution · epochs · watch · epoch 3

comparison

c_45dfa34511fe (tommy-mor) vs c_11d6a0ec9839 (tommy-mor)

download prompt · raw event · cmp_a1e497f9a90181

council reasoning

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

A does a genuine cross-cutting refactor: it introduces `room_route_segment`/`room_id_from_route_segment` helpers to replace ad-hoc split_once logic scattered across many files, plus a new URL-normalization module (with solid test coverage) that fixes real identity bugs like YouTube URL variants and query-order collisions. B is a smaller, useful bugfix (attribution leak, dead swap button, route rename) but touches far less surface and adds less lasting infrastructure than A's DRY refactor and normalization logic.

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

A lands lasting design: shared room route encode/decode (`room_route_segment` / `room_id_from_route_segment`), a simpler `/r/:room_key` surface wired through routes/nav/tests, plus real external URL identity normalization (YouTube rewrites, query sorting, case policy). B’s patches are correct but narrower—delegate attribution out-of-band, stop the bad <2-sibling fallback, drop swap, rename `/vote/compare`→`/vote`—valuable fixes without comparable structural weight.

openai/gpt-chat-latest · winner A · 5:2 · permalink

Side A introduces a substantive URL-routing refactor by centralizing private room path encoding/decoding (`room_route_segment`/`room_id_from_route_segment`), replacing ad hoc string splitting across handlers, routes, navigation, URL builders, and tests. It also adds reusable URL normalization utilities (including canonicalization for external URLs such as YouTube links), improving long-term consistency and maintainability, whereas Side B mainly fixes vote-page behavior, removes an unnecessary UI control, renames `/vote/compare` to `/vote`, and corrects delegate handling for vote posts.

sides

A — c_45dfa34511fe (tommy-mor)

message

[5ca518f6] url refactor

diff preview

diff --git a/Cargo.lock b/Cargo.lock
index 67a09a3b54f778fa7e857fdd589c3ed9c92e1322..ad7e4fe6d4ba2f2b033916194c1ef1ed873f1d46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1757,6 +1757,7 @@ name = "slug-types"
 version = "0.1.0"
 dependencies = [
  "serde",
+ "url",
 ]
 
 [[package]]
@@ -2272,6 +2273,7 @@ dependencies = [
  "idna",
  "percent-encoding",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 606f7d6f97a4428efb90d1e0d544934c861fc4c5..cd3e0f0afd972d9ad9e7e4b92c5fa4c22bb8f620 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -270,10 +270,10 @@ fn post_redirect_location(room: &str, thread_tag: &str) -> String {
         format!("/t/{tag}")
     } else {
         let room = room.trim();
-        let Some((a, b)) = room.split_once('/') else {
+        let Some(seg) = slug_types::room_route_segment(room) else {
             return "/".to_string();
         };
-        format!("/r/{a}/{b}/t/{tag}")
+        format!("/r/{seg}/t/{tag}")
     }
 }
 
diff --git a/server/src/api/write_actor.rs b/server/src/api/write_actor.rs
index cb78d2f3f95b1bc163c1fb064d1d5f657e18000f..f9c3b8bd3fbf8fcb9c035e1a1572fef0b08fa8a9 100644
--- a/server/src/api/write_actor.rs
+++ b/server/src/api/write_actor.rs
@@ -19,13 +19,15 @@ use crate::{
 use super::auth::{issue_token_for_user, verify_token};
 use super::helpers::{now_ms, resolve_item};
 use super::validate::{normalize_room_and_thread, validate_ingest_document};
-use slug_types::RpcResult;
+use slug_types::{room_route_segment, RpcResult, ROOM_SHORT_ID_LEN};
 
 fn gen_short_id() -> String {
     use rand::Rng;
     const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
     let mut rng = rand::thread_rng();
-    (0..7).map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char).collect()
+    (0..ROOM_SHORT_ID_LEN)
+        .map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char)
+        .collect()
 }
 
 fn parse_capability(s: &str) -> Result<crate::events::ThreadCapability, String> {
@@ -55,8 +57,8 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
     let feed_id = if room_key == "public" { "thread-feed" } else { "room-thread-feed" };
     let thread_url = if room_key == "public" {
         format!("/t/{thread_id}")
-    } else if let Some((short, slug)) = room_key.split_once('/') {
-        format!("/r/{short}/{slug}/t/{thread_id}")
+    } else if let Some(seg) = room_route_segment(room_key) {
+        format!("/r/{seg}/t/{thread_id}")
     } else {
         format!("/t/{thread_id}")
     };
@@ -78,8 +80,8 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
     let js = builder.build();
     let mut path_prefixes = vec![if room_key == "public" {
         "/".to_string()
-    } else if let Some((short, slug)) = room_key.split_once('/') {
-        format!("/r/{short}/{slug}")
+    } else if let Some(seg) = room_route_segment(room_key) {
+        format!("/r/{seg}")
     } else {
         "/".to_string()
     }];
diff --git a/server/src/html/forum/nav.rs b/server/src/html/forum/nav.rs
index 0ee33d91160fc5542817b5e3e9ab4fee1d0e600f..48fe11e46731670874ff8b6b05baa6f09ae0b7e4 100644
--- a/server/src/html/forum/nav.rs
+++ b/server/src/html/forum/nav.rs
@@ -1,7 +1,8 @@
 use crate::canonical_path::canonicalize_item;
 use crate::reducer::ScopeId;
+use slug_types::room_route_segment;
 
-/// URL helpers for public `/t/…` and private room threads `/r/{short}/{slug}/t/…`.
+/// URL helpers for public `/t/…` and private room threads `/r/{short}{slug}/t/…`.
 #[derive(Clone)]
 pub struct ThreadNav {
     pub room_wire: String,
@@ -22,18 +23,15 @@ impl ThreadNav {
         }
     }
 
-    /// `room_id` wire form `shortid/slug`.
+    /// `room_id` wire form `shortid/slug` (HTTP uses [`slug_types::room_route_segment`]).
     pub(crate) fn from_room_id(room_id: &str) -> Option<Self> {
-        let (short, slug) = room_id.split_once('/')?;
-        if short.is_empty() || slug.is_empty() {
-            return None;
-        }
+        let room_seg = room_route_segment(room_id)?;
         Some(Self {
             room_wire: room_id.to_string(),
             scope: ScopeId::Room(room_id.to_string()),
-            room_path: format!("/r/{short}/{slug}"),
-            thread_path_prefix: format!("/r/{short}/{slug}/t"),
-            garden_path_prefix: format!("/r/{short}/{slug}/~"),
+            room_path: format!("/r/{room_seg}"),
+            thread_path_prefix: format!("/r/{room_seg}/t"),
+            garden_path_prefix: format!("/r/{room_seg}/~"),
         })
     }
 
diff --git a/server/src/html/forum/post_single.rs b/server/src/html/forum/post_single.rs
index c316f8f836df9d4ef9c05ebd9e54f699540e6d72..473747b3da3d4d7a54b5e0c63165d2533df643e1 100644
--- a/server/src/html/forum/post_single.rs
+++ b/server/src/html/forum/post_single.rs
@@ -93,12 +93,14 @@ pub async fn thread_post_view(
 
 pub async fn room_thread_post_view(
     State(state): State<AppState>,
-    Path((room_short, room_slug, tag, index_str)): Path<(String, String, String, String)>,
+    Path((room_key, tag, index_str)): Path<(String, String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
diff --git a/server/src/html/forum/views.rs b/server/src/html/forum/views.rs
index be5df1745ef580891a167c23c3dd6c06f804f295..1ec421f84335cbfe7f9db775b73a8ed24b197174 100644
--- a/server/src/html/forum/views.rs
+++ b/server/src/html/forum/views.rs
@@ -183,16 +183,18 @@ pub async fn thread_view(
     thread_view_inner(state, tag, q, ThreadNav::public(), headers, jar, uri).await
 }
 
-/// Room thread — `/r/:short/:slug/t/:tag`
+/// Room thread — `/r/:room_key/t/:tag` (`room_key` = `{short}{slug}`).
 pub async fn room_thread_view(
     State(state): State<AppState>,
-    Path((room_short, room_slug, tag)): Path<(String, String, String)>,
+    Path((room_key, tag)): Path<(String, String)>,
     Query(q): Query<ThreadViewQuery>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
@@ -226,15 +228,17 @@ pub(super) fn room_not_found_page(jar: &CookieJar, uri: &Uri) -> impl IntoRespon
     (StatusCode::NOT_FOUND, Html(page.into_string()))
 }
 
-/// Private room index — `/r/:short/:slug`
+/// Private room index — `/r/:room_key`
 pub async fn room_page(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "room not found").into_response();
+    };
     let now = now_ms();
     let reduced = state.reduced.read().await;
     if !reduced.rooms.contains(&room_id) {
@@ -266,7 +270,10 @@ pub async fn room_page(
     let audit_cli = format!("npx slugsocial private {room_id} audit");
     drop(reduced);
 
-    let slug_display = room_slug.as_str();
+    let slug_display = room_id
+        .split_once('/')
+        .map(|(_, slug)| slug)
+        .unwrap_or(room_id.as_str());
     let page = layout(
         &format!("room {slug_display} — slug.social"),
         "view-thread",
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 423f23fd8c9ad7b7f454d6ea7a9a7607a4c9c5b9..e615dd356bcf634232d85610c0a26235ead125fd 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -309,12 +309,14 @@ pub async fn external_ontology_path(
 
 pub async fn room_garden_index(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -341,12 +343,14 @@ pub async fn room_garden_index(
 
 pub async fn room_external_garden_index(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -416,12 +420,14 @@ pub async fn room_external_garden_index(
 
 pub async fn room_external_ontology_path(
     State(state): State<AppState>,
-    Path((room_short, room_slug, path)): Path<(String, String, String)>,
+    Path((room_key, path)): Path<(String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -442,12 +448,14 @@ pub async fn room_external_ontology_path(
 
 pub async fn room_ontology_path(
     State(state): State<AppState>,
-    Path((room_short, room_slug, path)): Path<(String, String, String)>,
+    Path((room_key, path)): Path<(String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
diff --git a/server/src/html/search.rs b/server/src/html/search.rs
index 43e6ebf36cf0fe72c96f0f9d850bea51ac094c43..f01732f7edb32c68fcc10c39545c8f56476adb27 100644
--- a/server/src/html/search.rs
+++ b/server/src/html/search.rs
@@ -351,8 +351,8 @@ fn render_search_results(results: &SearchResults, query: &str) -> Markup {
                     ul class="search-posts" {
                         @for r in &results.posts {
                             @let (post_href, post_label) = if let Some((room, tag)) = r.thread.split_once("/#") {
-                                if let Some((short, slug)) = room.split_once('/') {
-                                    (format!("/r/{short}/{slug}/t/{tag}"), format!("{room}/#{tag}"))
+                                if let Some(seg) = slug_types::room_route_segment(room) {
+                           

… preview truncated; 35,812 characters omitted

download full diff A

B — c_11d6a0ec9839 (tommy-mor)

message

[36e92cf2] Fix /vote/compare page: attribution, fallback, swap button, and route rename.

- Pass WEB_BROWSER_AGENT as delegate_opt out-of-band (not in DSL body text)
- Error instead of falling back to all items when sibling pool < 2
- Remove pointless "swap sides" button
- Rename route /vote/compare → /vote

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 0b7070a2b7289e5c40778c8d062bd4150285a8df..8e3d8e34922ddaaa89ea699edd99402bd40fdb1b 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -281,6 +281,7 @@ pub async fn rpc_post_with_bearer(
     bearer_token: &str,
     room: String,
     thread_tag: String,
+    delegate_opt: Option<String>,
     text: String,
 ) -> Result<RpcResult, RpcErr> {
     use axum::http::{header, HeaderMap, HeaderValue};
@@ -288,7 +289,7 @@ pub async fn rpc_post_with_bearer(
     let hv = HeaderValue::from_str(&format!("Bearer {bearer_token}"))
         .map_err(|_| ("invalid session token".into(), None))?;
     headers.insert(header::AUTHORIZATION, hv);
-    rpc_post(state, &headers, room, thread_tag, None, text, false).await
+    rpc_post(state, &headers, room, thread_tag, delegate_opt, text, false).await
 }
 
 async fn rpc_check(
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index a13a94767d8905adc09e31effff838e166781b6c..5faa642451d69555cb391974beb0ad22c9355c8c 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -118,6 +118,7 @@ async fn dispatch_ui_action(
                 &session.bearer,
                 room.clone(),
                 thread_tag.clone(),
+                None,
                 text,
             )
             .await
@@ -246,8 +247,7 @@ async fn dispatch_ui_action(
             }
 
             let text = format!(
-                "@{}\n{{\n{}\n}}\n{} {}:{} {}\n",
-                crate::api::auth::WEB_BROWSER_AGENT,
+                "{{\n{}\n}}\n{} {}:{} {}\n",
                 exp,
                 left_id.as_str(),
                 rl,
@@ -260,6 +260,7 @@ async fn dispatch_ui_action(
                 &session.bearer,
                 room.clone(),
                 thread_tag.clone(),
+                Some(crate::api::auth::WEB_BROWSER_AGENT.to_string()),
                 text,
             )
             .await
diff --git a/server/src/html/forum/nav.rs b/server/src/html/forum/nav.rs
index d33dc50f0b89a5ed7730c3bca204a67f6309388f..ae67f550a4bdf0bbeff58384c29a715015b64bd5 100644
--- a/server/src/html/forum/nav.rs
+++ b/server/src/html/forum/nav.rs
@@ -85,7 +85,7 @@ impl ThreadNav {
         format!("{}/{}/{}", self.thread_path_prefix, tag, idx)
     }
 
-    /// Empty for public; `/r/:seg` for room — prefix for routes like `/vote/compare`.
+    /// Empty for public; `/r/:seg` for room — prefix for routes like `/vote`.
     pub(crate) fn room_path_prefix_for_vote_compare(&self) -> String {
         match &self.scope {
             ScopeId::Public => String::new(),
diff --git a/server/src/html/garden/vote.rs b/server/src/html/garden/vote.rs
index bdb4008c29ba49d7dc97b1cd2c32d9cdfce1ca04..1d7fc8aa7436bfa9c1186dfd940a8d751ab7e088 100644
--- a/server/src/html/garden/vote.rs
+++ b/server/src/html/garden/vote.rs
@@ -208,7 +208,7 @@ pub(crate) async fn vote_compare_post_success_js(
     state: &AppState,
     nav: &ThreadNav,
     _room_wire: &str,
-    thread_tag: &str,
+    _thread_tag: &str,
     left: &ItemId,
     right: &ItemId,
     _post_id: &str,
@@ -218,8 +218,7 @@ pub(crate) async fn vote_compare_post_success_js(
     let content = content_for_garden_view(&reduced, &nav.scope());
     let edge_history = vote_edge_history_markup(content, left, right);
     let next_pair = suggest_next_vote_pair(content, left, right);
-    let nav_markup =
-        vote_compare_nav_markup(nav, next_pair.as_ref(), left, right, Some(thread_tag));
+    let nav_markup = vote_compare_nav_markup(nav, next_pair.as_ref());
     drop(reduced);
     JsBuilder::new()
         .morph_inner_selector("#vote-edge-history-region", edge_history)
@@ -236,7 +235,7 @@ pub(super) fn vote_compare_href(
     let left_q = urlencoding::encode(left.as_str());
     let right_q = urlencoding::encode(right.as_str());
     let base = format!(
-        "{}/vote/compare?left={}&right={}",
+        "{}/vote?left={}&right={}",
         nav.room_path_prefix_for_vote_compare(),
         left_q,
         right_q
@@ -251,12 +250,8 @@ pub(super) fn vote_compare_href(
 fn vote_compare_nav_markup(
     nav: &ThreadNav,
     next_pair: Option<&(ItemId, ItemId)>,
-    left: &ItemId,
-    right: &ItemId,
-    thread_override: Option<&str>,
 ) -> maud::Markup {
     let next_pair_href = next_pair.map(|(nl, nr)| vote_compare_href(nav, nl, nr, None));
-    let swap_pair_href = vote_compare_href(nav, right, left, thread_override);
     html! {
         div class="vote-compare-nav" {
             @if let Some(href) = &next_pair_href {
@@ -264,7 +259,6 @@ fn vote_compare_nav_markup(
             } @else {
                 span class="vote-compare-next is-disabled" { "no next pair" }
             }
-            a class="vote-compare-next" href=(swap_pair_href) { "swap sides" }
         }
     }
 }
@@ -274,7 +268,7 @@ pub(super) fn suggest_next_vote_pair(
     current_left: &ItemId,
     current_right: &ItemId,
 ) -> Option<(ItemId, ItemId)> {
-    let mut pool: Vec<ItemId> = if current_left.parent().as_ref().map(|p| p.as_str())
+    let pool: Vec<ItemId> = if current_left.parent().as_ref().map(|p| p.as_str())
         == current_right.parent().as_ref().map(|p| p.as_str())
     {
         current_left
@@ -291,7 +285,7 @@ pub(super) fn suggest_next_vote_pair(
         Vec::new()
     };
     if pool.len() < 2 {
-        pool = content.items.iter().cloned().collect();
+        return None;
     }
     suggest_next_pair_in_pool(
         &content.ranking_group,
@@ -334,7 +328,7 @@ pub struct VoteCompareQuery {
     pub thread: Option<String>,
 }
 
-/// Public pairwise vote UI — `/vote/compare?left=&right=&thread=`.
+/// Public pairwise vote UI — `/vote?left=&right=&thread=`.
 pub async fn vote_compare_page(
     State(state): State<AppState>,
     Query(q): Query<VoteCompareQuery>,
@@ -426,7 +420,7 @@ async fn vote_compare_inner(
     let next_path = uri
         .path_and_query()
         .map(|pq| pq.as_str().to_string())
-        .unwrap_or_else(|| "/vote/compare".into());
+        .unwrap_or_else(|| "/vote".into());
 
     let rpc_json = template_json_compact(&json!({
         "action": "vote_compare_post",
@@ -462,7 +456,7 @@ async fn vote_compare_inner(
                 Some(&item_bodies_for_cards),
             ))
         }
-        (vote_compare_nav_markup(&nav, next_pair.as_ref(), &left, &right, q.thread.as_deref()))
+        (vote_compare_nav_markup(&nav, next_pair.as_ref()))
         div id="vote-edge-history-region" {
             (edge_history)
         }
diff --git a/server/src/html/ui_action.rs b/server/src/html/ui_action.rs
index c7b643c8dde37dd1e3001f5ff716fe4f79d24c5e..dd5294202569454acf9f41b458718700bbf96f39 100644
--- a/server/src/html/ui_action.rs
+++ b/server/src/html/ui_action.rs
@@ -38,7 +38,7 @@ pub enum HtmlUiAction {
         #[serde(default)]
         form_id: Option<String>,
     },
-    /// Post a pairwise vote from `/vote/compare` (browser compose).
+    /// Post a pairwise vote from `/vote` (browser compose).
     VoteComparePost {
         room: String,
         thread_tag: String,
diff --git a/server/src/lib.rs b/server/src/lib.rs
index ad8e31099c807fb5844acb16cd5086a2f19327a7..e102e73ec167fa35dcc22d4ec8c6f40202982429 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -82,9 +82,9 @@ pub fn create_app(state: AppState) -> Router {
         .route("/u/:username", get(crate::html::user_profile_page))
         .route("/try", get(crate::html::editor_page))
         .route("/try/check", post(crate::html::editor_check))
-        .route("/vote/compare", get(crate::html::vote_compare_page))
+        .route("/vote", get(crate::html::vote_compare_page))
         .route(
-            "/r/:room_key/vote/compare",
+            "/r/:room_key/vote",
             get(crate::html::room_vote_compare_page),
         )
         .route("/~", get(crate::html::garden_index))
diff --git a/server/tests/integration_html.rs b/server/tests/integration_html.rs
index a25a456c7cafd74a69e65e0c2e8ed2bfb2e27498..b3e31376737b2d5aabb739f408984a1f5ae0c974 100644
--- a/server/tests/integration_html.rs
+++ b/server/tests/integration_html.rs
@@ -52,12 +52,12 @@ async fn test_view_counts_increment_and_display() {
         .normalized_storage()
         .to_storage_string();
     let vote_q_right_first = format!(
-        "/vote/compare?right={}&left={}",
+        "/vote?right={}&left={}",
         urlencoding::encode(&right),
         urlencoding::encode(&left)
     );
     let vote_q_left_first = format!(
-        "/vote/compare?left={}&right={}",
+        "/vote?left={}&right={}",
         urlencoding::encode(&left),
         urlencoding::encode(&right)
     );
@@ -148,7 +148,7 @@ https://github.com/ghvotehi/a/issues/10 {\n\
         .normalized_storage()
         .to_storage_string();
     let q = format!(
-        "/vote/compare?left={}&right={}",
+        "/vote?left={}&right={}",
         urlencoding::encode(&left),
         urlencoding::encode(&right)
     );
diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs
index 091afd922d28f48ab36aa9b9084aa825036d1f74..23db7b5672418d5bb0ab7529e05ef1f89e59062a 100644
--- a/server/tests/integration_ui.rs
+++ b/server/tests/integration_ui.rs
@@ -110,7 +110,7 @@ async fn test_choose_username_carries_redirect_next() {
             .expect("pending session must exist");
         pending.provider = Some("google".to_string());
         pending.provider_id = Some("google-user-redirect".to_string());
-        pending.redirect_next = Some("/vote/compare?left=%7E%2Fa&right=%7E%2Fb".to_string());
+        pending.redirect_next = Some("/vote?left=%7E%2Fa&right=%7E%2Fb".to_string());
     }
 
     let choose = client
@@ -122,7 +122,7 @@ async fn test_choose_username_carries_redirect_next() {
 
     assert_eq!(choose.status(), reqwest::StatusCode::OK);
     let body = choose.text().await.unwrap();
-    assert!(body.contains("window.location = \"/vote/compare?left=%7E%2Fa&right=%7E%2Fb\""));
+    assert!(body.contains("window.location = \"/vote?left=%7E%2Fa&right=%7E%2Fb\""));
 }
 
 #[tokio::test]
diff --git a/test/browser_vote_compare.clj b/test/browser_vote_compare.clj
index 7cd1e467fdd8a4480df1be2afd78adb45b9cdc6d..a98e144756421e1fa5064db6a9fc0dd8b08471c7 100644
--- a/test/browser_vote_compare.clj
+++ b/test/browser_vote_compare.clj
@@ -66,7 +66,7 @@
                       :headers {"Authorization" (str "Bearer " alice-token)})
            post-json (json/parse-string (:body post-resp) false)
            _ (is (true? (get-in post-json ["results" 0 "ok"])) "seed items + edge vote via rpc")
-           cmp-url (str base-url "/vote/compare?left=" (enc left-url) "&right=" (enc third-url))]
+           cmp-url (str base-url "/vote?left=" (enc left-url) "&right=" (enc third-url))]
        (core/with-playwright [pw]
          (core/with-browser [browser (core/launch-chromium pw {:headless true :channel "chrome"})]
            (core/with-context [ctx (core/new-context browser)]

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.