Side A performs a substantive architectural consolidation: it removes the redundant web_post.rs endpoints (/post, /post/check, /post/redact), unifies all browser mutations behind the single POST /ui + __rpc__ dispatch, introduces a session-resolution helper (WebSession/resolve_web_session) to avoid duplicate header/cookie parsing, and updates templates, JS, tests, and fixtures to match—an end-to-end simplification of the routing surface. Side B is a much smaller, localized cleanup that trims unused parameters/logic from vote_compare_post_success_js and removes a dead card-render call, which is useful but minor in comparison.
constitution · epochs · watch · epoch 3
c_effff18688f9 (tommy-mor) vs c_7b940fef005f (tommy-mor)
download prompt · raw event · cmp_0963d8a41913fe
council reasoning
A consolidates browser ingest/check/redact onto a single POST /ui path: adds WebSession/resolve_web_session, deletes web_post.rs and the /post* routes, inlines dispatch through RPC, and updates forms, client check JS, and tests—lasting structural cleanup. B only trims unused args and drops a #vote-compare-preview morph in vote_compare_post_success_js, a small local simplification with little design impact.
Side A consolidates the browser posting flow behind a single `POST /ui` RPC endpoint, introduces `resolve_web_session` to resolve username and bearer once, removes the duplicated `web_post.rs` handlers, updates forms, routes, and tests, and preserves functionality while simplifying the architecture. Side B is a narrow cleanup that removes unused parameters and an unnecessary preview update from `vote_compare_post_success_js`, improving maintainability but with much smaller project-wide impact.
sides
A — c_effff18688f9 (tommy-mor)
message
[c3cbcaa7] refactor
diff preview
diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index b3631b06153d52f88348fef927e7a024b7b85ad6..cd556eb89e9dd8203eba6c8969ffd144db5d329d 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -71,6 +71,24 @@ pub fn optional_principal(headers: &HeaderMap, jar: &CookieJar, reduced: &Reduce
verify_token(reduced, c.value()).ok()
}
+/// Browser session: principal + bearer token string (same shape as CLI session cookie).
+#[derive(Debug, Clone)]
+pub struct WebSession {
+ pub username: String,
+ pub bearer: String,
+}
+
+/// Resolve username and bearer together for `POST /ui` dispatch (one read of headers + jar).
+pub fn resolve_web_session(headers: &HeaderMap, jar: &CookieJar, reduced: &ReducerState) -> Option<WebSession> {
+ let username = optional_principal(headers, jar, reduced)?;
+ let bearer = headers
+ .get(header::AUTHORIZATION)
+ .and_then(|v| v.to_str().ok())
+ .and_then(|s| s.strip_prefix("Bearer ").map(|t| t.trim().to_string()))
+ .or_else(|| jar.get(SLUG_SESSION_COOKIE).map(|c| c.value().to_string()))?;
+ Some(WebSession { username, bearer })
+}
+
fn redirect_with_session_cookie(public_url: &str, path_and_query: &str, bearer: &str, jar: &CookieJar) -> Response {
let mut res = Response::builder()
.status(StatusCode::TEMPORARY_REDIRECT)
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index a986f706ea4b261cbaf004c02b4cf84184b41371..4e223a7460997c464706dca850281447bd754ed5 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -4,7 +4,6 @@ mod rpc;
mod stream;
mod validate;
mod ui_html;
-mod web_post;
pub use auth::{
get_join_invite,
@@ -19,7 +18,9 @@ pub use auth::{
get_web_login,
get_logout,
optional_principal,
+ resolve_web_session,
session_cookie_header_value,
+ WebSession,
SLUG_SESSION_COOKIE,
};
@@ -35,7 +36,6 @@ pub use stream::{get_html_stream, get_stream};
pub use validate::{normalize_room_and_thread, validate_ingest_document, ValidatedIngest};
pub use ui_html::post_ui_html;
-pub use web_post::{check_web_ingest, post_web_ingest, post_web_redact};
#[cfg(test)]
mod tests {
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 2b40a72059981d558768f73d189b991f3448c257..497f8fdd222a8ec7c3d76b0695e0352e973ca3ba 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -1,25 +1,29 @@
-//! Single `POST /ui` entry for browser [`crate::html::ui_action::HtmlUiAction`] (JSON in `__rpc__` + holes).
+//! Single `POST /ui` entry: parse `__rpc__` → [`HtmlUiAction`], resolve [`WebSession`] once, dispatch.
use axum::{
- body::Body,
+ body,
extract::State,
- http::{header, HeaderMap, StatusCode},
+ http::{header, HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Form,
};
use axum_extra::extract::cookie::CookieJar;
+use slug_types::{RpcBatch, RpcBatchResponse, RpcCommand, RpcResult};
use std::collections::HashMap;
use crate::{
api::{
- auth::optional_principal,
- web_post::{run_check_web_ingest, run_post_web_ingest, run_post_web_redact, WebPostForm, WebRedactForm},
+ auth::{resolve_web_session, WebSession},
+ handle_rpc_batch,
+ rpc::{rpc_post_redact, rpc_post_with_bearer},
},
+ canonical_path::canonicalize_tag,
html::{
fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
- parse_html_ui_from_form, user_can_post_room, user_can_view_room, HtmlUiAction, JsBuilder,
- ThreadNav,
+ parse_html_ui_from_form, thread_feed_html, thread_feed_html_for_room, thread_feed_region_markup,
+ user_can_post_room, user_can_view_room, HtmlUiAction, JsBuilder, ThreadNav,
},
+ reducer::{scope_from_room_wire, ScopeId},
state::AppState,
};
@@ -34,6 +38,19 @@ pub async fn post_ui_html(
Err(e) => return ui_js_warn(&e.to_string()).into_response(),
};
+ let reduced = state.reduced.read().await;
+ let session = resolve_web_session(&headers, &jar, &reduced);
+ drop(reduced);
+
+ dispatch_ui_action(&state, session.as_ref(), action).await
+}
+
+/// All UI command logic: HTTP extractors stop above; this only sees [`AppState`], session, and [`HtmlUiAction`].
+async fn dispatch_ui_action(
+ state: &AppState,
+ session: Option<&WebSession>,
+ action: HtmlUiAction,
+) -> Response {
match action {
HtmlUiAction::PostIngest {
room,
@@ -42,47 +59,87 @@ pub async fn post_ui_html(
error_target,
form_id,
} => {
- run_post_web_ingest(
- &state,
- &headers,
- &jar,
- WebPostForm {
- room,
- thread_tag,
- text,
- error_target,
- form_id,
- },
- )
- .await
+ let Some(session) = session else {
+ return js_redirect("/login").into_response();
+ };
+ let room = room.trim().to_string();
+ let thread_tag = thread_tag.trim().to_string();
+ if text.trim().is_empty() {
+ return form_js_error(
+ error_target.as_ref(),
+ "empty post",
+ "Write something in the text area (DSL / prose).",
+ )
+ .into_response();
+ }
+ match rpc_post_with_bearer(state, &session.bearer, room.clone(), thread_tag.clone(), text).await {
+ Ok(RpcResult::PostOk { .. }) => {
+ post_success_response(
+ state,
+ &room,
+ &thread_tag,
+ error_target.as_ref(),
+ form_id.as_ref(),
+ Some(session.username.as_str()),
+ )
+ .await
+ .into_response()
+ }
+ Ok(_) => form_js_error(
+ error_target.as_ref(),
+ "unexpected response",
+ "Post did not return PostOk.",
+ )
+ .into_response(),
+ Err((msg, hint)) => form_js_error(error_target.as_ref(), &msg, hint.as_deref().unwrap_or("")).into_response(),
+ }
}
HtmlUiAction::CheckIngest {
room,
thread_tag,
text,
error_target,
- form_id,
+ form_id: _,
} => {
- run_check_web_ingest(
- &state,
- &headers,
- &jar,
- WebPostForm {
- room,
- thread_tag,
- text,
- error_target,
- form_id,
- },
- )
- .await
+ let Some(session) = session else {
+ return js_redirect("/login").into_response();
+ };
+ let room = room.trim().to_string();
+ let thread_tag = canonicalize_tag(&thread_tag);
+ if thread_tag.is_empty() {
+ return form_js_error(
+ error_target.as_ref(),
+ "missing thread tag",
+ "Set a thread tag before posting.",
+ )
+ .into_response();
+ }
+ if text.trim().is_empty() {
+ return js_clear_errors(&form_error_target(error_target.as_ref())).into_response();
+ }
+ match rpc_check_with_bearer(state, &session.bearer, room, text.clone()).await {
+ Ok(RpcResult::CheckOk { .. }) => js_clear_errors(&form_error_target(error_target.as_ref())).into_response(),
+ Ok(_) => form_js_error(error_target.as_ref(), "unexpected response", "Check did not return CheckOk.").into_response(),
+ Err((msg, hint)) => form_js_error(error_target.as_ref(), &msg, hint.as_deref().unwrap_or("")).into_response(),
+ }
}
HtmlUiAction::RedactPost { post_id } => {
- run_post_web_redact(&state, &headers, &jar, WebRedactForm { post_id }).await
+ let Some(session) = session else {
+ return js_redirect("/login").into_response();
+ };
+ let h = headers_from_bearer(&session.bearer);
+ match rpc_post_redact(state, &h, post_id).await {
+ Ok(RpcResult::RedactPostOk {}) => redact_success_response(state).await.into_response(),
+ Ok(_) => (StatusCode::BAD_REQUEST, "unexpected response").into_response(),
+ Err((msg, hint)) => {
+ let detail = hint.as_deref().unwrap_or("");
+ js_error("#errors", &msg, detail).into_response()
+ }
+ }
}
HtmlUiAction::ExpandPublicNewThreadForm => {
let reduced = state.reduced.read().await;
- let user = optional_principal(&headers, &jar, &reduced);
+ let user = session.map(|s| s.username.as_str());
drop(reduced);
let markup = if user.is_some() {
fragment_public_new_thread_form(true)
@@ -99,18 +156,18 @@ pub async fn post_ui_html(
return ui_js_warn("missing room").into_response();
}
let reduced = state.reduced.read().await;
- let user = optional_principal(&headers, &jar, &reduced);
+ let user = session.map(|s| s.username.as_str());
if !reduced.rooms.contains(&room_wire) {
drop(reduced);
return ui_js_warn("room not found").into_response();
}
- if !user_can_view_room(&reduced, &room_wire, user.as_deref()) {
+ if !user_can_view_room(&reduced, &room_wire, user) {
drop(reduced);
return ui_js_warn("forbidden").into_response();
}
- let can_post = user
+ let can_post = session
.as_ref()
- .map(|u| user_can_post_room(&reduced, &room_wire, u))
+ .map(|s| user_can_post_room(&reduced, &room_wire, &s.username))
.unwrap_or(false);
drop(reduced);
let Some(nav) = ThreadNav::from_room_id(&room_wire) else {
@@ -128,12 +185,195 @@ pub async fn post_ui_html(
}
}
+fn headers_from_bearer(bearer: &str) -> HeaderMap {
+ let mut headers = HeaderMap::new();
+ if let Ok(hv) = HeaderValue::from_str(&format!("Bearer {bearer}")) {
+ headers.insert(header::AUTHORIZATION, hv);
+ }
+ headers
+}
+
+fn post_redirect_location(room: &str, thread_tag: &str) -> String {
+ let tag = canonicalize_tag(thread_tag);
+ if room.trim() == "public" {
+ format!("/t/{tag}")
+ } else {
+ let room = room.trim();
+ let Some((a, b)) = room.split_once('/') else {
+ return "/".to_string();
+ };
+ format!("/r/{a}/{b}/t/{tag}")
+ }
+}
+
+fn js_quote(s: &str) -> String {
+ serde_json::to_string(s).expect("js string escaping must succeed")
+}
+
+fn js_redirect(to: &str) -> Response {
+ let js = format!("window.location = {};", js_quote(to));
+ Response::builder()
+ .status(StatusCode::OK)
+ .header(header::CONTENT_TYPE, "text/javascript; charset=utf-8")
+ .body(axum::body::Body::from(js))
+ .unwrap()
+}
+
+fn js_error(error_target: &str, title: &str, detail: &str) -> Response {
+ let markup = maud::html! {
+ div id=(error_target.trim_start_matches('#')) {
+ p class="auth-error" { (title) }
+ @if !detail.is_empty() {
+ pre class="muted" { (detail) }
+ }
+ }
+ };
+ JsBuilder::new()
+ .morph_selector(error_targe
… preview truncated; 33,445 characters omittedB — c_7b940fef005f (tommy-mor)
message
[c3f01e01] cleanup
diff preview
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index f9912115d3c6ac18be66c952c3af445ab0f51cea..5aa6a86326ba3545d261322c010e02fe86ee1c57 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -254,17 +254,7 @@ async fn dispatch_ui_action(
};
n
};
- let js = vote_compare_post_success_js(
- state,
- &nav,
- &room,
- &thread_tag,
- &left_id,
- &right_id,
- &pid,
- post_index,
- )
- .await;
+ let js = vote_compare_post_success_js(state, &nav, &left_id, &right_id).await;
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/javascript; charset=utf-8")
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 7d309a075405483971109da2f34160409dc608f7..8a46b1e92d38f2c390f88d48750d95d11388cd73 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -176,34 +176,14 @@ fn vote_edge_history_markup(
pub(crate) async fn vote_compare_post_success_js(
state: &AppState,
nav: &ThreadNav,
- room_wire: &str,
- thread_tag: &str,
left: &ItemId,
right: &ItemId,
- post_id: &str,
- post_idx: Option<usize>,
) -> String {
- use crate::reducer::scope_from_room_wire;
let reduced = state.reduced.read().await;
- let scope = scope_from_room_wire(room_wire.trim());
- 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 card = ingest_entry_markup(nav, thread_tag, idx, &ing, viewer, now, &reduced);
let content = content_for_garden_view(&reduced, &nav.scope());
let edge_history = vote_edge_history_markup(content, left, right, nav);
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()
}
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.