B restores a real, tested feature (page view counts) end-to-end: a new ViewStore, canonicalization logic for query-order-insensitive counting, middleware wiring, and integration tests verifying counts increment and dedupe correctly across permuted URLs. A is a substantial UX rework (login->account page) with reasonable structure, but it's mostly UI/markup/CSS churn with one test assertion removed rather than strengthened, and no verification of the new alias-switching/claim flows added by the diff.
constitution · epochs · watch · epoch 3
c_164969eeea14 (tommy-mor) vs c_55d1158891d1 (tommy-mor)
download prompt · raw event · cmp_ad86da5c8acca1
council reasoning
B restores cross-cutting ViewStore infrastructure (middleware increments, query canonicalization, AppState wiring) and wires real view counts into garden/forum/search/try layouts with integration tests replacing a stub—lasting platform behavior. A is a solid localized auth UX win (account page, alias switch/claim, OAuth re-link, shared claim forms) but does not add comparable systemic durability beyond /login and CSS.
Side B restores a cross-cutting feature by wiring a persistent ViewStore into AppState, adding GET middleware that canonicalizes query parameters before counting views, and propagating real view counts into many page layouts with integration tests verifying both incrementing and canonical URL behavior. Side A substantially improves the account UI by turning /login into an account management page with alias switching, alias claiming reuse, and OAuth relinking, but it is primarily a user-interface and flow enhancement rather than foundational infrastructure.
sides
A — c_164969eeea14 (tommy-mor)
message
[d6b4a41e] Turn /login into an account page with alias switching. Signed-in users can see trust weight, switch aliases, claim new ones, and re-link OAuth from one place. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/server/src/auth/mod.rs b/server/src/auth/mod.rs
index d4a85ef52c15dc35148e4c743f0d646cbbdb056d..5906f93b13853421e96a3c37bc9d8202a47842bf 100644
--- a/server/src/auth/mod.rs
+++ b/server/src/auth/mod.rs
@@ -94,63 +94,169 @@ fn alias_list(db: &durable::Db, uuid: &str) -> Vec<String> {
.unwrap_or_default()
}
-fn login_body(
- session: Option<&session::SessionActor>,
+fn alias_claim_forms(return_to: &str, submit_label: &str) -> Result<Markup, StatusCode> {
+ let check_rpc = template_json_compact(&serde_json::json!({
+ "action": "check_pseudonym",
+ "pseudonym": {"$form": "pseudonym"},
+ }))
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+ let claim_rpc = template_json_compact(&serde_json::json!({
+ "action": "claim_pseudonym",
+ "pseudonym": {"$form": "pseudonym"},
+ "return_to": return_to,
+ }))
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ Ok(html! {
+ div class="alias-claim" {
+ form id="alias-check-form" method="POST" action="/ui" {
+ input type="hidden" name=(UI_RPC_FIELD) value=(check_rpc);
+ label for="alias-input" { "alias" }
+ input type="text" id="alias-input" name="pseudonym" autocomplete="off"
+ data-testid="alias-input" maxlength="64" placeholder="letters, numbers, _ -";
+ p id="alias-status" class="muted" data-testid="alias-status" { "type to check availability" }
+ }
+ form id="alias-claim-form" method="POST" action="/ui" {
+ input type="hidden" name=(UI_RPC_FIELD) value=(claim_rpc);
+ input type="hidden" name="pseudonym" id="alias-claim-field" value="";
+ button type="submit" class="btn-primary" data-testid="alias-claim" { (submit_label) }
+ }
+ }
+ })
+}
+
+fn signed_out_body(providers: &[(&str, String)]) -> Markup {
+ html! {
+ main class="panel login-page" {
+ section class="login-section" {
+ h1 { "sign in" }
+ p class="muted" { "link an account to vote under a lasting alias" }
+ @if providers.is_empty() {
+ p class="muted" {
+ "OAuth is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET."
+ }
+ } @else {
+ ul class="oauth-provider-list" {
+ @for (name, href) in providers {
+ li {
+ a href=(href) class="btn-primary oauth-provider"
+ data-testid=(format!("oauth-{}", name.to_lowercase())) {
+ (format!("Continue with {name}"))
+ }
+ }
+ }
+ }
+ }
+ }
+ p class="login-back" { a href="/" { "← back" } }
+ }
+ }
+}
+
+fn account_body(
+ actor: &session::SessionActor,
aliases: &[String],
providers: &[(&str, String)],
+ claim_forms: Markup,
) -> Markup {
+ let current = actor.pseudonym.trim();
html! {
- main class="panel login-page" {
- div class="login-grid" {
- section class="login-oauth" {
- h1 { "sign in" }
- @if providers.is_empty() {
- p class="muted" {
- "OAuth is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET."
- }
- } @else {
- ul class="oauth-provider-list" {
- @for (name, href) in providers {
- li {
- a href=(href) class="button oauth-provider" data-testid=(format!("oauth-{}", name.to_lowercase())) {
- (format!("Continue with {name}"))
+ main class="panel login-page account-page" {
+ section class="login-section" {
+ h1 { "account" }
+ @if current.is_empty() {
+ p class="muted" { "finish setup by choosing an alias below" }
+ } @else {
+ p class="account-current" {
+ "voting as "
+ strong data-testid="account-current" { (current) }
+ }
+ }
+ p class="muted small" data-testid="account-weight" {
+ "trust weight " (format!("{:.1}", actor.trust_weight))
+ " · rises when you link more OAuth providers"
+ }
+ }
+
+ section class="login-section" {
+ h2 { "aliases" }
+ @if aliases.is_empty() {
+ p class="muted" data-testid="alias-list-empty" { "none yet — claim one below" }
+ } @else {
+ ul id="alias-list" class="alias-list" data-testid="alias-list" {
+ @for alias in aliases {
+ @let is_current = alias == current;
+ li class=(if is_current { "alias-item alias-current" } else { "alias-item" }) {
+ span class="alias-name" { (alias) }
+ @if is_current {
+ span class="alias-badge" data-testid="alias-current-badge" { "current" }
+ } @else {
+ form class="alias-switch" method="post" action="/auth/switch"
+ data-navigate="full" {
+ input type="hidden" name="pseudonym" value=(alias);
+ button type="submit" class="btn-secondary"
+ data-testid=(format!("alias-switch-{alias}")) {
+ "use"
+ }
}
}
}
}
}
- @if let Some(actor) = session {
- p class="muted small" {
- "session active · weight " (format!("{:.1}", actor.trust_weight))
- }
- form method="post" action="/auth/logout" data-navigate="full" {
- button type="submit" { "log out" }
- }
- }
}
- section class="login-aliases" {
- h2 { "your aliases" }
- ul id="alias-list" class="alias-list" {
- @if aliases.is_empty() {
- li class="muted" data-testid="alias-list-empty" { "none yet" }
- } @else {
- @for alias in aliases {
- li { (alias) }
+ }
+
+ section class="login-section" {
+ h2 { "add alias" }
+ p class="muted small" { "each alias is unique across sorter2" }
+ (claim_forms)
+ }
+
+ @if !providers.is_empty() {
+ section class="login-section" {
+ h2 { "linked sign-in" }
+ p class="muted small" { "sign in again with the same provider to return to this account" }
+ ul class="oauth-provider-list" {
+ @for (name, href) in providers {
+ li {
+ a href=(href) class="btn-secondary oauth-provider"
+ data-testid=(format!("oauth-relink-{}", name.to_lowercase())) {
+ (format!("Re-link {name}"))
+ }
}
}
}
}
}
- p { a href="/" { "← back" } }
+
+ section class="login-section login-actions" {
+ form method="post" action="/auth/logout" data-navigate="full" {
+ button type="submit" class="btn-secondary" data-testid="account-logout" { "log out" }
+ }
+ }
+
+ p class="login-back" { a href="/" { "← back" } }
}
}
}
+fn login_body(
+ session: Option<&session::SessionActor>,
+ aliases: &[String],
+ providers: &[(&str, String)],
+ claim_forms: Option<Markup>,
+) -> Markup {
+ match (session, claim_forms) {
+ (Some(actor), Some(forms)) => account_body(actor, aliases, providers, forms),
+ _ => signed_out_body(providers),
+ }
+}
+
pub async fn login_page(
State(state): State<AppState>,
jar: CookieJar,
Query(query): Query<LoginQuery>,
-) -> Response {
+) -> Result<Response, StatusCode> {
let return_to = return_from_query_or_jar(&jar, query.return_to.as_deref());
let jar = jar.add(session::auth_return_cookie_value(&return_to));
@@ -164,16 +270,26 @@ pub async fn login_page(
.unwrap_or_default();
let providers = oauth_providers(&base_url_from_env(state.cfg.port), &return_to);
+ let claim_forms = if session.is_some() {
+ Some(alias_claim_forms("/login", "claim alias")?)
+ } else {
+ None
+ };
+
let markup = layout(
- "login · sorter2",
- login_body(session.as_ref(), &aliases, &providers),
+ if session.is_some() {
+ "account · sorter2"
+ } else {
+ "login · sorter2"
+ },
+ login_body(session.as_ref(), &aliases, &providers, claim_forms),
state.views.get_views("/login"),
session
.as_ref()
.filter(|s| !s.pseudonym.trim().is_empty())
.map(|s| s.pseudonym.as_str()),
);
- (jar, Html(markup.into_string())).into_response()
+ Ok((jar, Html(markup.into_string())).into_response())
}
pub async fn alias_page(
@@ -186,38 +302,17 @@ pub async fn alias_page(
let db = state.projection_store.db();
let session = session::load_valid_session(db, &session_id).ok_or(StatusCode::UNAUTHORIZED)?;
if session::session_has_pseudonym(&session) {
- return Ok(Redirect::to(&return_to).into_response());
+ // Already onboarded — manage aliases on the account page.
+ return Ok(Redirect::to("/login").into_response());
}
- let check_rpc = template_json_compact(&serde_json::json!({
- "action": "check_pseudonym",
- "pseudonym": {"$form": "pseudonym"},
- }))
- .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
- let claim_rpc = template_json_compact(&serde_json::json!({
- "action": "claim_pseudonym",
- "pseudonym": {"$form": "pseudonym"},
- "return_to": return_to,
- }))
- .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
+ let claim_forms = alias_claim_forms(&return_to, "continue")?;
let body = html! {
main class="panel alias-page" {
h1 { "choose alias" }
p class="muted" { "pick a unique display name for your votes" }
- form id="alias-check-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(check_rpc);
- label { "alias" }
- input type="text" id="alias-input" name="pseudonym" autocomplete="off"
- data-testid="alias-input" maxlength="64";
- p id="alias-status" class="muted" data-testid="alias-status" { "type to check availability" }
- }
- form id="alias-claim-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(claim_rpc);
- input type="hidden" nam
… preview truncated; 4,534 characters omittedB — 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 omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.