constitution · epochs · watch · epoch 3

comparison

c_55d1158891d1 (tommy-mor) vs c_01304e7d0f46 (tommy-mor)

download prompt · raw event · cmp_2468422174e759

council reasoning

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

Side A restores a real feature (persistent view counts) with new state (ViewStore), middleware wiring, canonical URL normalization, and a concrete integration test verifying counts and canonicalization—substantive new functionality. Side B is a clean but purely internal refactor unifying duplicate new-thread form markup/ids across public/room scopes, valuable for maintainability but lower-impact than adding a working feature with tests.

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

A restores a full product capability: ViewStore on AppState, GET middleware with path filters, query canonicalization, view counts wired through garden/forum/search/try layouts, and real integration assertions—lasting infrastructure and UX. B is a worthwhile but narrower maintainability win that unifies public/room new-thread DOM ids and HtmlUiAction variants without adding new user-facing behavior.

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

Side A restores a substantive feature by introducing a persistent ViewStore into AppState, adding middleware to count GET page views with canonicalized URLs, wiring view counts into page layouts, and replacing a stub test with integration tests that verify counting and query-order canonicalization. Side B is primarily a UI refactor that unifies public and room new-thread forms, selectors, and actions under shared IDs, improving maintainability but with less direct long-term functional impact than reintroducing durable view counting.

sides

A — c_55d1158891d1 (tommy-mor)

message

[d6da7856] Resurrect page view counts (ViewStore + middleware + layout) (#138)

* Wire ViewStore through AppState, view-count middleware, and HTML.

- Add views module to lib, ViewStore on AppState (create_app_state and tests).
- Implement canonical_view_url and GET view_count_middleware with path filters.
- Layer middleware before with_state; add url crate for query canonicalization.
- Pass canonical-key view counts into garden, forum, search, and try layouts.
- Replace stub integration test with real view counter assertions.

Co-authored-by: tommy <thmorriss@gmail.com>

* Test vote/compare query canonicalization instead of search.

Vote compare uses chromeless layout without view badge; assert the shared
ViewStore count for permuted left/right query order via AppState.

Co-authored-by: tommy <thmorriss@gmail.com>

* Browser test: stop waiting for removed vote-compare shell.

Assert vote compare via body.view-vote-compare and compare heading instead
of .vote-compare-shell, which was intentionally removed from the HTML.

Co-authored-by: tommy <thmorriss@gmail.com>

* Drop vote-compare preview morph and browser assertion.

vote_compare_post_success_js now only refreshes #vote-edge-history-region.
Remove preview wrap markup, browser test wait on #vote-compare-preview, and
unused forum re-export of ingest_entry_markup.

Co-authored-by: tommy <thmorriss@gmail.com>

---------

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

diff preview

diff --git a/Cargo.lock b/Cargo.lock
index ad7e4fe6d4ba2f2b033916194c1ef1ed873f1d46..bf8153d9c723af97122c9ffdd4a7cfe82e853bb6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1805,6 +1805,7 @@ dependencies = [
  "tower-http 0.5.2",
  "tracing",
  "tracing-subscriber",
+ "url",
  "urlencoding",
  "uuid",
 ]
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6d85b8cce5701482367d4be43287394e26f5896f..0d9cdcd21d94751babdf769e118d2c89d6a009d8 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -28,6 +28,7 @@ slug-types = { path = "../types" }
 async-trait = "0.1"
 base64 = "0.22"
 postcard = { version = "1", features = ["use-std"] }
+url = "2"
 urlencoding = "2"
 
 [dev-dependencies]
diff --git a/server/src/html/editor.rs b/server/src/html/editor.rs
index a24e638802c8bb38699b39fdb0c53c22183a4a49..8ff58326eb75806eeec3937f088fa977c7f3886e 100644
--- a/server/src/html/editor.rs
+++ b/server/src/html/editor.rs
@@ -9,8 +9,9 @@ use maud::{html, Markup};
 use serde::Deserialize;
 
 use crate::{
-    api::{validate_ingest_document, resolve_item},
+    api::{resolve_item, validate_ingest_document},
     html::JsBuilder,
+    middleware::canonical_view_url,
     reducer::ScopeId,
     state::AppState,
 };
@@ -25,7 +26,10 @@ fn bc_try() -> Markup {
 }
 
 /// The interactive editor page — `/try`.
-pub async fn editor_page(jar: CookieJar, uri: Uri) -> impl IntoResponse {
+pub async fn editor_page(State(state): State<AppState>, jar: CookieJar, uri: Uri) -> impl IntoResponse {
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         "try — slug.social",
         "view-thread",
@@ -41,7 +45,7 @@ pub async fn editor_page(jar: CookieJar, uri: Uri) -> impl IntoResponse {
                 div id="editor-results" {}
             }
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 63f196e0df251678bd649da97107e35a741f97fa..70321dd2a75e563282f7612f2b2fa6d57b9175d9 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -8,6 +8,7 @@ use maud::{html, Markup};
 
 use crate::api::optional_principal;
 use crate::canonical_path::canonicalize_tag;
+use crate::middleware::canonical_view_url;
 use crate::reducer::{ReducerState, ScopeId};
 use crate::state::AppState;
 use crate::timeago;
@@ -218,6 +219,9 @@ pub async fn home(
     let strip = auth_strip(&headers, &jar, &reduced_read);
     drop(reduced_read);
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         "slug.social",
         "view-thread",
@@ -251,7 +255,7 @@ pub async fn home(
             (render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
             (cli_panel(&["npx slugsocial public forum list"]))
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
diff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs
index 53809e94e3b107a1ddb937c2e45325fc6b8706c6..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644
--- a/server/src/html/forum/mod.rs
+++ b/server/src/html/forum/mod.rs
@@ -20,7 +20,6 @@ pub use profile::user_profile_page;
 pub use views::{room_page, room_thread_view, thread_view};
 
 pub(crate) use access::{user_can_post_room, user_can_view_room};
-pub(crate) use ingest::ingest_entry_markup;
 pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
 pub(crate) use room_members::room_members_section_markup;
 pub(crate) use thread_morph::{
diff --git a/server/src/html/forum/post_single.rs b/server/src/html/forum/post_single.rs
index 6e36e80d076ddd8ca521c1450cbf8cb7ba6a28fe..91866b2cc3dea98d0c3847f54379245eca67894c 100644
--- a/server/src/html/forum/post_single.rs
+++ b/server/src/html/forum/post_single.rs
@@ -8,6 +8,7 @@ use maud::html;
 
 use crate::api::optional_principal;
 use crate::canonical_path::canonicalize_tag;
+use crate::middleware::canonical_view_url;
 use crate::reducer::ScopeId;
 use crate::state::AppState;
 
@@ -70,6 +71,9 @@ async fn thread_post_view_inner(
         }
     };
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         &format!("#{tag} / post #{index}"),
         "view-thread",
@@ -87,7 +91,7 @@ async fn thread_post_view_inner(
                 p class="muted" { "post not found" }
             }
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
diff --git a/server/src/html/forum/profile.rs b/server/src/html/forum/profile.rs
index ebdc41fdd44e97a10a49ecd1ac5150ae390ec319..94701f8e9e293b64b889291c2145f97c1078c1c4 100644
--- a/server/src/html/forum/profile.rs
+++ b/server/src/html/forum/profile.rs
@@ -9,6 +9,7 @@ use maud::html;
 use crate::api::optional_principal;
 use crate::canonical_path::canonicalize_tag;
 use crate::identity::parse_username;
+use crate::middleware::canonical_view_url;
 use crate::state::AppState;
 
 use super::ingest::{thread_nav_for_ingest, thread_post_index_in_scope};
@@ -74,6 +75,9 @@ pub async fn user_profile_page(
     };
 
     let now = now_ms();
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         &format!("@{canon}"),
         "view-thread",
@@ -108,7 +112,7 @@ pub async fn user_profile_page(
             }
             (cli_panel(&[format!("npx slugsocial public forum list")]))
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
diff --git a/server/src/html/forum/views.rs b/server/src/html/forum/views.rs
index 34c47a2a6838a0ec99415e326cbd78f2cc988bd7..b363f0d7c8af3d941e4f402c1f1b87100bb0d394 100644
--- a/server/src/html/forum/views.rs
+++ b/server/src/html/forum/views.rs
@@ -11,6 +11,7 @@ use serde_json::json;
 use crate::api::optional_principal;
 use crate::canonical_path::canonicalize_tag;
 use crate::form_template::template_json_compact;
+use crate::middleware::canonical_view_url;
 use crate::reducer::ScopeId;
 use crate::state::AppState;
 
@@ -141,6 +142,9 @@ async fn thread_view_inner(
         ScopeId::Room(r) => format!("npx slugsocial private {r} forum show {tag}"),
     };
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let body = html! {
         (strip)
         nav class="breadcrumb" { (bc) }
@@ -166,7 +170,7 @@ async fn thread_view_inner(
         &format!("#{tag}"),
         "view-thread",
         body,
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
@@ -276,6 +280,9 @@ pub async fn room_page(
     let audit_cli = format!("npx slugsocial private {room_id} audit");
     drop(reduced);
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let slug_display = room_id
         .split_once('/')
         .map(|(_, slug)| slug)
@@ -296,7 +303,7 @@ pub async fn room_page(
             }
             (cli_panel(&[forum_cli, garden_cli, audit_cli]))
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         None,
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 2d5fcd903fe8f56800ad2c5564b704bd25f9b18d..9e08307e636ce7984766168db1187f84b8635201 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -14,11 +14,12 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE
 use crate::{
     api::optional_principal,
     canonical_path::{canonicalize_item, canonicalize_tag},
+    middleware::canonical_view_url,
     events::ThreadCapability,
     form_template::template_json_compact,
     html::{JsBuilder, ui_action::UI_RPC_FIELD, user_can_post_room},
     path_types::ItemId,
-    reducer::{ContentState, ReducerState, ScopeId, scope_from_room_wire},
+    reducer::{ContentState, ReducerState, ScopeId},
     scope_rank::{ChildrenRankings, build_children_rankings},
     state::AppState,
     timeago,
@@ -28,7 +29,7 @@ use super::{
     bc_path, bc_path_external, bc_segment,
     breadcrumb_path::{ExternalOntologyPath, OntologyPath},
     cli_panel,
-    forum::{ThreadNav, ingest_entry_markup},
+    forum::ThreadNav,
     layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_linkified_with_embeds_in_scope,
     theme_from_jar, theme_next_from_uri,
 };
@@ -224,39 +225,24 @@ fn vote_edge_history_markup(content: &ContentState, left: &ItemId, right: &ItemI
     }
 }
 
-/// After a successful vote post: morph the new card into `#vote-compare-preview` and refresh edge history.
+/// After a successful vote post: refresh edge history (no in-page preview card).
 pub(crate) async fn vote_compare_post_success_js(
     state: &AppState,
     nav: &ThreadNav,
-    room_wire: &str,
-    thread_tag: &str,
+    _room_wire: &str,
+    _thread_tag: &str,
     left: &ItemId,
     right: &ItemId,
-    post_id: &str,
-    post_idx: Option<usize>,
+    _post_id: &str,
+    _post_idx: Option<usize>,
 ) -> String {
     let reduced = state.reduced.read().await;
-    let scope = scope_from_room_wire(room_wire);
-    let Some(ing) = reduced.ingests_by_id.get(post_id).cloned() else {
-        drop(reduced);
-        return "console.warn('vote compare: new post not found');".to_string();
-    };
-    let idx = match post_idx {
-        Some(i) => i,
-        None => reduced
-            .try_thread_post_index_chronological(&scope, thread_tag, post_id)
-            .unwrap_or(0),
-    };
-    let viewer = None::<&str>;
-    let now = now_ms();
     let content = content_for_garden_view(&reduced, &nav.scope());
     let edge_history = vote_edge_history_markup(content, left, right);
-    let card = ingest_entry_markup(nav, thread_tag, idx, &ing, viewer, now, &reduced);
     drop(reduced);
-    let mut b = JsBuilder::new();
-    b = b.morph_inner_selector("#vote-compare-preview", card);
-    b = b.morph_inner_selector("#vote-edge-history-region", edge_history);
-    b.build()
+    JsBuilder::new()
+        .morph_inner_selector("#vote-edge-history-region", edge_history)
+        .build()
 }
 
 fn item_display_path(item: &str) -> String {
@@ -515,6 +501,9 @@ pub async fn garden_index(
         build_children_rankings(reduced.public(), &ItemId::ontology_root())
     };
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         "~/",
         "view-ontology view-ontology-light",
@@ -556,7 +545,7 @@ pub async fn garden_index(
             }
             (cli_panel(&["npx slugsocial garden tree"]))
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         Some("public"),
@@ -597,6 +586,9 @@ pub async fn external_garden_index(
         build_children_rankings(reduced.public(), &parent)
     };
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         "-/",
         "view-ontology view-ontology-light",
@@ -637,7 +629,7 @@ pub async fn external_garden_index(
                 }
             }
         },
-        None,
+        Some(view_count),
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
         Some("public"),
@@ -727,6 +719,9 @@ pub async fn room_external_garden_index(
         build_children_rankings(content_for_garden_view(&reduced, &nav.scope()), &parent);
     drop(reduced);
 
+    let url_key = canonical_view_url(&uri);
+    let view_count = state.views.get_views(&url_key);
+
     let page = layout(
         "-/",
 

… preview truncated; 10,577 characters omitted

download full diff A

B — c_01304e7d0f46 (tommy-mor)

message

[d88719a1] refactor(html): unify public and room new-thread forms

- Single DOM contract: #new-thread-ui-slot, #new-thread-compose, #new-thread-form,
  #new-thread-tag; shared check_ingest + post_ingest error wiring for all scopes.
- Replace ExpandPublic/ExpandRoom/SetRoomNewThreadComposeExpanded with
  ExpandNewThreadForm { room_wire } and SetNewThreadComposeExpanded { room_wire, expanded };
  room_wire "public" covers the home toolbar flow.
- SSE refresh resets #new-thread-compose form for any room_key.
- Browser test selectors updated for the unified ids.

Made-with: Cursor

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 03f4f7efe08df3ec160e50d08652bde7d6a9c4d2..422b520f527f64571f86f0ee3cb57edcbc21c0c3 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -63,11 +63,7 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
         crate::html::thread_feed_region_markup(state, Some(room_key), thread_id, None).await;
 
     let builder = JsBuilder::new().morph_selector(&format!("#{feed_id}"), feed_markup);
-    let builder = if room_key == "public" {
-        builder.qs("#public-new-thread-compose form").reset()
-    } else {
-        builder.qs("#room-new-thread-compose form").reset()
-    };
+    let builder = builder.qs("#new-thread-compose form").reset();
     let builder = builder.if_current_path_matches(&thread_url, |builder| {
         builder.morph_selector("#thread-feed-region", thread_feed_markup)
     });
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 6ff9f1319e91f61bf8546eb65637c20f3a58483c..e979053ff1ba8c0e2bf55add0a32b7de11cf1e56 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -19,7 +19,7 @@ use crate::{
     },
     canonical_path::canonicalize_tag,
     html::{
-        fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
+        fragment_new_thread_slot, login_to_post_hint_markup,
         parse_html_ui_from_form, room_members_section_markup, thread_feed_html,
         thread_feed_html_for_room, thread_feed_region_markup, thread_ui_collapse_redacted_post,
         thread_ui_expand_post_full, thread_ui_expand_redacted_post, ui_js_warn, user_can_post_room,
@@ -139,24 +139,24 @@ async fn dispatch_ui_action(
                 }
             }
         }
-        HtmlUiAction::ExpandPublicNewThreadForm => {
-            let reduced = state.reduced.read().await;
-            let user = session.map(|s| s.username.as_str());
-            drop(reduced);
-            let markup = if user.is_some() {
-                fragment_public_new_thread_form(true)
-            } else {
-                login_to_post_hint_markup()
-            };
-            JsBuilder::new()
-                .morph_inner_selector("#public-new-thread-ui-slot", markup)
-                .into_response()
-        }
-        HtmlUiAction::ExpandRoomNewThreadForm { room_wire } => {
+        HtmlUiAction::ExpandNewThreadForm { room_wire } => {
             let room_wire = room_wire.trim().to_string();
             if room_wire.is_empty() {
                 return ui_js_warn("missing room").into_response();
             }
+            if room_wire == "public" {
+                let reduced = state.reduced.read().await;
+                let user = session.map(|s| s.username.as_str());
+                drop(reduced);
+                let markup = if user.is_some() {
+                    fragment_new_thread_slot(&ThreadNav::public(), true, false)
+                } else {
+                    login_to_post_hint_markup()
+                };
+                return JsBuilder::new()
+                    .morph_inner_selector("#new-thread-ui-slot", markup)
+                    .into_response();
+            }
             let reduced = state.reduced.read().await;
             let user = session.map(|s| s.username.as_str());
             if !reduced.rooms.contains(&room_wire) {
@@ -176,12 +176,12 @@ async fn dispatch_ui_action(
                 return ui_js_warn("bad room").into_response();
             };
             let markup = if can_post {
-                fragment_room_new_thread_form(&nav, true, false)
+                fragment_new_thread_slot(&nav, true, false)
             } else {
                 login_to_post_hint_markup()
             };
             JsBuilder::new()
-                .morph_inner_selector("#room-new-thread-ui-slot", markup)
+                .morph_inner_selector("#new-thread-ui-slot", markup)
                 .into_response()
         }
         HtmlUiAction::SetRoomMembersExpanded { room_wire, expanded } => {
@@ -205,11 +205,28 @@ async fn dispatch_ui_action(
                 .morph_selector("#room-members-section", markup)
                 .into_response()
         }
-        HtmlUiAction::SetRoomNewThreadComposeExpanded { room_wire, expanded } => {
+        HtmlUiAction::SetNewThreadComposeExpanded { room_wire, expanded } => {
             let room_wire = room_wire.trim().to_string();
             if room_wire.is_empty() {
                 return ui_js_warn("missing room").into_response();
             }
+            if room_wire == "public" {
+                let reduced = state.reduced.read().await;
+                let _user = session.map(|s| s.username.as_str());
+                drop(reduced);
+                let can_post = session.is_some();
+                let nav = ThreadNav::public();
+                let markup = if can_post {
+                    fragment_new_thread_slot(&nav, true, expanded)
+                } else {
+                    login_to_post_hint_markup()
+                };
+                let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
+                if expanded && can_post {
+                    b = b.focus_selector("#new-thread-tag");
+                }
+                return b.into_response();
+            }
             let reduced = state.reduced.read().await;
             let user = session.map(|s| s.username.as_str());
             if !reduced.rooms.contains(&room_wire) {
@@ -229,13 +246,13 @@ async fn dispatch_ui_action(
                 return ui_js_warn("bad room").into_response();
             };
             let markup = if can_post {
-                fragment_room_new_thread_form(&nav, true, expanded)
+                fragment_new_thread_slot(&nav, true, expanded)
             } else {
                 login_to_post_hint_markup()
             };
-            let mut b = JsBuilder::new().morph_inner_selector("#room-new-thread-ui-slot", markup);
+            let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
             if expanded && can_post {
-                b = b.focus_selector("#room-new-tag");
+                b = b.focus_selector("#new-thread-tag");
             }
             b.into_response()
         }
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 30b95ff6e85245a82199c96e41484d03c24989dd..1b4ae7baa3ad4757b74172d7b67f3f2b33d1075d 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -245,11 +245,13 @@ pub async fn home(
             p class="muted" { "dark = time-ordered · light = vote-ranked" }
             div class="thread-feed-toolbar" {
                 form method="POST" action="/ui" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect("static json"));
+                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandNewThreadForm {
+                        room_wire: "public".into(),
+                    }).expect("static json"));
                     button type="submit" class="section-add-btn" { "+" }
                 }
             }
-            div id="public-new-thread-ui-slot" {}
+            div id="new-thread-ui-slot" {}
             (render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
             (cli_panel(&["npx slugsocial public forum list"]))
         },
diff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs
index 8fbf3b71f41fafb474999ebb3d622e6bd5b5f313..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644
--- a/server/src/html/forum/mod.rs
+++ b/server/src/html/forum/mod.rs
@@ -20,9 +20,7 @@ pub use profile::user_profile_page;
 pub use views::{room_page, room_thread_view, thread_view};
 
 pub(crate) use access::{user_can_post_room, user_can_view_room};
-pub(crate) use new_thread::{
-    fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
-};
+pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
 pub(crate) use room_members::room_members_section_markup;
 pub(crate) use thread_morph::{
     thread_ui_collapse_redacted_post, thread_ui_expand_post_full, thread_ui_expand_redacted_post,
diff --git a/server/src/html/forum/new_thread.rs b/server/src/html/forum/new_thread.rs
index b2d24b8ab3ec7eee0edd16bed8ee6605e49bf408..4dd2654f953a7db6ee9b25c348f99906da93a94a 100644
--- a/server/src/html/forum/new_thread.rs
+++ b/server/src/html/forum/new_thread.rs
@@ -5,120 +5,48 @@ use serde_json::json;
 
 use super::nav::ThreadNav;
 
-struct NewThreadIds {
-    compose_section_id: &'static str,
-    errors_id: &'static str,
-    form_id: &'static str,
-    tag_input_id: &'static str,
-    text_input_id: Option<&'static str>,
-}
-
-const PUBLIC_IDS: NewThreadIds = NewThreadIds {
-    compose_section_id: "public-new-thread-compose",
-    errors_id: "public-new-thread-errors",
-    form_id: "public-new-thread-form",
-    tag_input_id: "new-thread-tag",
-    text_input_id: Some("new-thread-text"),
-};
-
-const ROOM_IDS: NewThreadIds = NewThreadIds {
-    compose_section_id: "room-new-thread-compose",
-    errors_id: "room-new-thread-errors",
-    form_id: "room-new-thread-form",
-    tag_input_id: "room-new-tag",
-    text_input_id: None,
-};
-
-#[derive(Clone, Copy)]
-enum NewThreadComposeKind {
-    /// Home page: no client-side check RPC; post template omits `error_target` / `form_id`.
-    Public,
-    /// Room page: `check_ingest` + error targets on post (matches thread compose).
-    Room,
-}
-
-/// Shared `<section class="compose">` for creating a thread + first post.
-fn new_thread_compose_section(room_wire: &str, ids: &NewThreadIds, kind: NewThreadComposeKind) -> Markup {
-    let client_check = matches!(kind, NewThreadComposeKind::Room);
-    let (tag_placeholder, text_placeholder, submit_label) = match kind {
-        NewThreadComposeKind::Public => (
-            "thread-title-slug-here",
-            "Hello threadgoers!! Behold my new thread!",
-            "create thread / make first post",
-        ),
-        NewThreadComposeKind::Room => (
-            "thread-topic-slug-here",
-            "First post body…",
-            "post",
-        ),
-    };
+/// Stable ids shared by public home and private room “new thread” UI (`#new-thread-ui-slot`).
+const COMPOSE_SECTION_ID: &str = "new-thread-compose";
+const ERRORS_ID: &str = "new-thread-errors";
+const FORM_ID: &str = "new-thread-form";
+const TAG_INPUT_ID: &str = "new-thread-tag";
 
+/// Shared `<section class="compose">`: check + post with `error_target` / `form_id` (same as thread reply compose).
+fn new_thread_compose_section(room_wire: &str) -> Markup {
     html! {
-        section class="compose" id=(ids.compose_section_id) {
-            div id=(ids.errors_id) {}
-            @if client_check {
-                form id=(ids.form_id) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
-                    "action": "check_ingest",
+        section class="compose" id=(COMPOSE_SECTION_ID) {
+            div id=(ERRORS_ID) {}
+            form id=(FORM_ID) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
+                "action": "check_ingest",
+                "room": room_wire,
+                "thread_tag": {"$form": "thread_tag"},
+                "text": {"$form": "text"},
+                "error_target": ERRORS_ID,
+                "form_id": FORM_ID,
+            })).unwrap()) {
+                input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({
+                    "action": "post_ingest",
                     "room": room_wire,
                     "thread_tag": {"$form": "thread_tag"},
                     "text": {"$form": "text"},
-                    "error_target": ids.errors_id,
-             

… preview truncated; 13,556 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.