Commit A performs a meaningful structural refactor, splitting a monolithic forum.rs into cohesive modules (nav, ingest, new_thread, room_members, access), reducing duplication (e.g. removing separate rpc-value helper functions) and adding a genuinely useful sample-fixture dev tool. Commit B is a small, focused CSS/markup tweak moving the delete button's position, which is a real but narrow UI fix with much less lasting architectural value.
constitution · epochs · watch · epoch 3
c_59974b9da42a (tommy-mor) vs c_fc17a4aa31e6 (tommy-mor)
download prompt · raw event · cmp_135055b528467d
council reasoning
A’s lasting value is the forum split into focused modules (nav, access, ingest, new_thread, room_members) that structure real domain boundaries; B only relocates the delete control into the meta row with flex CSS. Minor noise in A (sample-fixture script, RPC helper inlining) does not outweigh that architectural win over B’s small UI polish.
Side A performs a substantial architectural refactor by extracting forum functionality into focused modules (such as access, ingest, navigation, room members, and new thread handling) and centralizing reusable logic like URL helpers and permission checks, improving maintainability without changing behavior. It also adds a useful developer profiling script (`sample-fixture`) and removes redundant helper wrappers by inlining simple RPC generation, whereas Side B mainly repositions the delete button within the post metadata and adjusts CSS layout for that UI change.
sides
A — c_59974b9da42a (tommy-mor)
message
[c99adc26] refactor forum into files
diff preview
diff --git a/bb.edn b/bb.edn
index 4680e82d4227057d2eea6041dc5647beb759131d..ce0f6fcd45a67ead3295b90db2a301e9b304f3da 100644
--- a/bb.edn
+++ b/bb.edn
@@ -51,6 +51,11 @@
:requires ([test.walkthrough-fixture :as walkthrough-fixture])
:task (walkthrough-fixture/run-fixture)}
+ sample-fixture
+ {:doc "macOS: run `sample` on process(es) listening on the fixture TCP port (default 8080). Usage: bb sample-fixture [PORT] [DURATION_SEC] [OUT_DIR]"
+ :requires ([scripts.sample-fixture :as sample-fixture])
+ :task (apply sample-fixture/-main *command-line-args*)}
+
perf
{:doc "Performance test: concurrent HTTP requests to detect blocking I/O"
:requires ([scripts.perf :as perf])
diff --git a/scripts/sample_fixture.bb b/scripts/sample_fixture.bb
new file mode 100644
index 0000000000000000000000000000000000000000..ac2368623c5043ba6e027afef6b5b37d392767b0
--- /dev/null
+++ b/scripts/sample_fixture.bb
@@ -0,0 +1,70 @@
+(ns scripts.sample-fixture
+ "Find process(es) listening on the fixture port (default 8080) and run macOS `sample`."
+ (:require [babashka.fs :as fs]
+ [babashka.process :as p]
+ [clojure.string :as str]))
+
+(defn- usage []
+ (println "Usage: bb sample-fixture [PORT] [DURATION_SEC] [OUT_DIR]")
+ (println "")
+ (println " Finds PIDs bound to TCP LISTEN on PORT (default 8080), then runs")
+ (println " `sample` for each PID. OUT_DIR defaults to the current directory.")
+ (println "")
+ (println " Example: bb sample-fixture")
+ (println " bb sample-fixture 8080 10")
+ (println " bb sample-fixture 8080 5 /tmp")
+ (println "")
+ (println " Requires macOS (the `sample` tool)."))
+
+(defn- parse-long* [s]
+ (try (Long/parseLong s)
+ (catch NumberFormatException _ nil)))
+
+(defn- listen-pids [port]
+ (let [spec (str "TCP:" port)
+ {:keys [out exit]}
+ @(p/process ["lsof" "-nP" (str "-i" spec) "-sTCP:LISTEN" "-t"]
+ {:out :string :err :string})]
+ (when (zero? exit)
+ (->> (str/split-lines out)
+ (map str/trim)
+ (remove str/blank?)
+ (distinct)
+ vec))))
+
+(defn- sample-bin []
+ (or (fs/which "sample")
+ (throw (ex-info "macOS `sample` not found on PATH" {}))))
+
+(defn- run-sample! [sample duration-sec pid out-file]
+ (println (str "sampling PID " pid " for " duration-sec "s → " out-file))
+ (let [{:keys [exit err]} @(p/process [sample (str pid) (str duration-sec) "-file" out-file]
+ {:out :inherit :err :inherit})]
+ (when-not (zero? exit)
+ (binding [*out* *err*]
+ (println "sample failed:" err))
+ (System/exit exit))))
+
+(defn -main [& args]
+ (when (some #{"-h" "--help" "help"} args)
+ (usage)
+ (System/exit 0))
+ (let [port (or (some-> (first args) parse-long*) 8080)
+ duration-sec (or (some-> (second args) parse-long*) 5)
+ out-dir (or (nth args 2 nil) ".")
+ pids (listen-pids port)]
+ (when (or (nil? pids) (empty? pids))
+ (binding [*out* *err*]
+ (println (str "No process listening on TCP " port " (LISTEN). Is `bb fixture` running?")))
+ (System/exit 1))
+ (when-not (fs/exists? out-dir)
+ (binding [*out* *err*]
+ (println "Output directory does not exist:" out-dir))
+ (System/exit 1))
+ (let [sample (sample-bin)
+ ts (str (System/currentTimeMillis))]
+ (println (str "port " port " → PIDs " (str/join ", " pids)))
+ (doseq [pid pids]
+ (let [out-file (str (fs/path out-dir) "/slug-sample-" port "-" pid "-" ts ".txt")]
+ (run-sample! sample duration-sec pid (str out-file))))
+ (println "done."))))
diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
index a55fc1f79d03719eb74b216865b2c15199c48ea7..5ad8dfc84dd735d589432e2c613ff687a75f2e61 100644
--- a/server/src/html/forum.rs
+++ b/server/src/html/forum.rs
@@ -380,22 +380,6 @@ fn room_members_inner(members: &[RoomMemberRow]) -> Markup {
}
}
-pub(crate) fn set_room_members_expanded_rpc(room_wire: &str, expanded: bool) -> String {
- template_json_compact(&HtmlUiAction::SetRoomMembersExpanded {
- room_wire: room_wire.to_string(),
- expanded,
- })
- .expect("static json")
-}
-
-pub(crate) fn set_room_new_thread_compose_expanded_rpc(nav: &ThreadNav, expanded: bool) -> String {
- template_json_compact(&HtmlUiAction::SetRoomNewThreadComposeExpanded {
- room_wire: nav.room_wire.clone(),
- expanded,
- })
- .expect("static json")
-}
-
/// Fragment for `#room-members-section` — expand/collapse is server-driven via `POST /ui`.
pub(crate) fn room_members_section_markup(
reduced: &ReducerState,
@@ -406,13 +390,14 @@ pub(crate) fn room_members_section_markup(
if members.is_empty() {
return html! {};
}
- let rpc_open = set_room_members_expanded_rpc(room_id, true);
- let rpc_close = set_room_members_expanded_rpc(room_id, false);
html! {
div id="room-members-section" {
@if members_expanded {
form method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(rpc_close);
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomMembersExpanded {
+ room_wire: room_id.to_string(),
+ expanded: false,
+ }).expect("static json"));
button type="submit" class="form-toggle" aria-expanded="true" {
"hide members & permissions"
}
@@ -422,7 +407,10 @@ pub(crate) fn room_members_section_markup(
}
} @else {
form method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(rpc_open);
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomMembersExpanded {
+ room_wire: room_id.to_string(),
+ expanded: true,
+ }).expect("static json"));
button type="submit" class="form-toggle" aria-expanded="false" {
"members & permissions"
}
@@ -514,28 +502,24 @@ fn compose_form(nav: &ThreadNav, thread_tag: &str, show: bool) -> Markup {
if !show {
return html! {};
}
- let rpc_post = template_json_compact(&json!({
- "action": "post_ingest",
- "room": nav.room_wire,
- "thread_tag": thread_tag,
- "text": {"$form": "text"},
- "error_target": "thread-compose-errors",
- "form_id": "thread-compose-form",
- }))
- .unwrap();
- let rpc_check = template_json_compact(&json!({
- "action": "check_ingest",
- "room": nav.room_wire,
- "thread_tag": thread_tag,
- "text": {"$form": "text"},
- "error_target": "thread-compose-errors",
- "form_id": "thread-compose-form",
- }))
- .unwrap();
html! {
section class="compose" id="thread-compose" {
- form id="thread-compose-form" method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(rpc_check) {
- input type="hidden" name=(UI_RPC_FIELD) value=(rpc_post);
+ form id="thread-compose-form" method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
+ "action": "check_ingest",
+ "room": nav.room_wire,
+ "thread_tag": thread_tag,
+ "text": {"$form": "text"},
+ "error_target": "thread-compose-errors",
+ "form_id": "thread-compose-form",
+ })).unwrap()) {
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({
+ "action": "post_ingest",
+ "room": nav.room_wire,
+ "thread_tag": thread_tag,
+ "text": {"$form": "text"},
+ "error_target": "thread-compose-errors",
+ "form_id": "thread-compose-form",
+ })).unwrap());
textarea name="text" rows="5" cols="80" placeholder="prose or ~/items and votes…" {}
p {
button type="submit" { "post" }
@@ -712,7 +696,7 @@ 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=(expand_public_new_thread_rpc_value());
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect("static json"));
button type="submit" class="section-add-btn" { "+" }
}
}
@@ -1002,14 +986,15 @@ fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool)
if !show {
return html! {};
}
- let rpc_open = set_room_new_thread_compose_expanded_rpc(nav, true);
- let rpc_close = set_room_new_thread_compose_expanded_rpc(nav, false);
// Single root for Idiomorph when morphing `#room-new-thread-ui-slot` (expanded has form + section).
html! {
div class="room-new-thread-slot-inner" {
@if compose_expanded {
form method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(rpc_close);
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomNewThreadComposeExpanded {
+ room_wire: nav.room_wire.clone(),
+ expanded: false,
+ }).expect("static json"));
button type="submit" class="form-toggle" aria-expanded="true" {
"-"
}
@@ -1039,7 +1024,10 @@ fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool)
}
} @else {
form method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(rpc_open);
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomNewThreadComposeExpanded {
+ room_wire: nav.room_wire.clone(),
+ expanded: true,
+ }).expect("static json"));
button type="submit" class="form-toggle" aria-expanded="false" {
"+"
}
@@ -1049,10 +1037,6 @@ fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool)
}
}
-pub(crate) fn expand_public_new_thread_rpc_value() -> String {
- template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect("static json")
-}
-
pub(crate) fn login_to_post_hint_markup() -> Markup {
html! {
p class="muted" { "log in to post" }
diff --git a/server/src/html/forum/access.rs b/server/src/html/forum/access.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f9f7139b63442a6ef3a927b77a72805f15feec3f
--- /dev/null
+++ b/server/src/html/forum/access.rs
@@ -0,0 +1,16 @@
+use crate::events::ThreadCapability;
+use crate::reducer::ReducerState;
+
+pub(crate) fn user_can_view_room(reduced: &ReducerState, room_id: &str, username: Option<&str>) -> bool {
+ if !reduced.rooms.contains(room_id) {
+ return false;
+ }
+ let Some(u) = username else {
+ return false;
+ };
+ reduced.user_has_cap(room_id, u, ThreadCapability::View)
+}
+
+pub(crate) fn user_can_post_room(reduced: &ReducerState, room_id: &str, username: &str) -> bool {
+ reduced.user_has_cap(room_id, username, ThreadCapability::Post)
+}
diff --git a/server/src/html/forum/ingest
… preview truncated; 19,729 characters omittedB — c_fc17a4aa31e6 (tommy-mor)
message
[af6c4464] moved delete button
diff preview
diff --git a/server/src/html/forum/ingest.rs b/server/src/html/forum/ingest.rs
index 168106e495cf1a81f62f6a49e35c64beea287924..308358abc6cd6811079d9ec41b114093c0294d02 100644
--- a/server/src/html/forum/ingest.rs
+++ b/server/src/html/forum/ingest.rs
@@ -45,6 +45,7 @@ fn post_header_meta(
principal: &str,
ts: i64,
now: i64,
+ delete_post_id: Option<&str>,
) -> Markup {
let post_href = nav.post_url(tag, post_idx);
let profile = profile_href(principal);
@@ -52,11 +53,19 @@ fn post_header_meta(
let ago = timeago::timeago(now, ts);
html! {
div class="ingest-meta muted" title=(hover) {
- a href=(post_href) class="post-num" { "#" (post_idx) }
- " "
- a href=(profile) class="post-author" { "@" (principal) }
- " · "
- (ago)
+ span class="ingest-meta-primary" {
+ a href=(post_href) class="post-num" { "#" (post_idx) }
+ " "
+ a href=(profile) class="post-author" { "@" (principal) }
+ " · "
+ (ago)
+ }
+ @if let Some(pid) = delete_post_id {
+ form class="post-delete-form" method="POST" action="/ui" {
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::RedactPost { post_id: pid.to_string() }).unwrap());
+ button type="submit" class="post-delete-btn" { "delete" }
+ }
+ }
}
}
}
@@ -70,18 +79,12 @@ pub(super) fn post_header_row(
now: i64,
show_delete: bool,
) -> Markup {
- let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
- html! {
- div class="ingest-header-row" {
- (meta)
- @if show_delete {
- form class="post-delete-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::RedactPost { post_id: ing.id.clone() }).unwrap());
- button type="submit" class="post-delete-btn" { "delete" }
- }
- }
- }
- }
+ let delete_post_id = if show_delete {
+ Some(ing.id.as_str())
+ } else {
+ None
+ };
+ post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now, delete_post_id)
}
pub(super) fn redacted_header_row(
@@ -92,7 +95,7 @@ pub(super) fn redacted_header_row(
now: i64,
expanded: bool,
) -> Markup {
- let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
+ let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now, None);
let rpc_expand = template_json_compact(&json!({
"action": "expand_redacted_post",
"room": nav.room_wire,
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 560881c1eec62f12d1e66875287be3c2f39198fc..83b49d079e6bf3e47c27fbd443a0ae75b1a03051 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -552,12 +552,21 @@ pre a.pre-link {
}
div.ingest-meta {
+ align-items: center;
background: var(--g3);
border-bottom: 2px solid var(--lo);
color: var(--meta);
+ display: flex;
+ flex-wrap: wrap;
font-size: 12px;
+ gap: 6px 10px;
+ justify-content: space-between;
padding: 3px 10px;
}
+span.ingest-meta-primary {
+ flex: 1 1 auto;
+ min-width: 0;
+}
a.post-num { color: var(--meta); font-size: 12px; }
a.post-num:hover { color: var(--signal); }
a.post-author { color: var(--meta); font-size: 12px; text-decoration: none; }
@@ -579,7 +588,12 @@ div.ingest-header-row div.ingest-meta {
flex: 1 1 auto;
padding: 0;
}
-form.post-delete-form { display: inline; margin: 0; }
+form.post-delete-form {
+ display: block;
+ flex-shrink: 0;
+ margin: 0;
+ margin-left: auto;
+}
button.post-delete-btn {
background: var(--g4);
border: var(--bv) solid;
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index eb022a8c84c1d0cff36410541872d68a11195e31..7181c7f8547940a402ae0211220fd6ee93ee1364 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -132,8 +132,22 @@ p.post-truncation-action {
margin: 0;
}
div.ingest-meta {
+ align-items: center;
border-bottom: 1px solid var(--line);
- padding: 0.35rem 0.65rem;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding-left: 0.35rem;
+}
+span.ingest-meta-primary {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+form.post-delete-form {
+ display: block;
+ flex-shrink: 0;
+ margin: 0;
+ margin-left: auto;
}
a.post-num {
color: var(--ink-dim);
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.