subdomain routing works

Orual 8b6741eb 6e274038

+193 -171
+1
crates/weaver-app/src/components/app_link.rs
··· 75 #[component] 76 pub fn AppLink(props: AppLinkProps) -> Element { 77 let link_mode = use_context::<LinkMode>(); 78 let class = props.class.clone().unwrap_or_default(); 79 80 match link_mode {
··· 75 #[component] 76 pub fn AppLink(props: AppLinkProps) -> Element { 77 let link_mode = use_context::<LinkMode>(); 78 + tracing::info!(?link_mode, "AppLink: reading LinkMode context"); 79 let class = props.class.clone().unwrap_or_default(); 80 81 match link_mode {
+8 -8
crates/weaver-app/src/components/entry.rs
··· 1 #![allow(non_snake_case)] 2 3 - use crate::components::{AppLink, AppLinkTarget}; 4 - use crate::Route; 5 #[cfg(feature = "server")] 6 use crate::blobcache::BlobCache; 7 use crate::components::AuthorList; 8 use crate::{components::EntryActions, data::use_handle}; 9 use dioxus::prelude::*; 10 use jacquard::types::aturi::AtUri; ··· 32 ) -> Element { 33 // Use feature-gated hook for SSR support 34 let (entry_res, entry) = crate::data::use_entry_data(ident, book_title, title); 35 - let route = use_route::<Route>(); 36 - let mut last_route = use_signal(|| route.clone()); 37 38 #[cfg(all( 39 target_family = "wasm", ··· 47 let mut entry_res = entry_res?; 48 49 #[cfg(feature = "fullstack-server")] 50 - use_effect(use_reactive!(|route| { 51 - if route != last_route() { 52 - tracing::debug!("[EntryPage] route changed, restarting resource"); 53 entry_res.restart(); 54 - last_route.set(route.clone()); 55 } 56 })); 57
··· 1 #![allow(non_snake_case)] 2 3 #[cfg(feature = "server")] 4 use crate::blobcache::BlobCache; 5 use crate::components::AuthorList; 6 + use crate::components::{AppLink, AppLinkTarget}; 7 use crate::{components::EntryActions, data::use_handle}; 8 use dioxus::prelude::*; 9 use jacquard::types::aturi::AtUri; ··· 31 ) -> Element { 32 // Use feature-gated hook for SSR support 33 let (entry_res, entry) = crate::data::use_entry_data(ident, book_title, title); 34 + 35 + // Track props for change detection (works with both Route and SubdomainRoute) 36 + let mut last_title = use_signal(|| title().clone()); 37 38 #[cfg(all( 39 target_family = "wasm", ··· 47 let mut entry_res = entry_res?; 48 49 #[cfg(feature = "fullstack-server")] 50 + use_effect(use_reactive!(|title| { 51 + if title != last_title() { 52 + tracing::debug!("[EntryPage] title changed, restarting resource"); 53 entry_res.restart(); 54 + last_title.set(title()); 55 } 56 })); 57
+14 -17
crates/weaver-app/src/components/login.rs
··· 4 use jacquard::oauth::session::ClientData; 5 use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr}; 6 7 - use crate::{CONFIG, Route}; 8 - use crate::{ 9 - components::{ 10 - button::{Button, ButtonVariant}, 11 - dialog::{DialogContent, DialogRoot, DialogTitle}, 12 - input::Input, 13 - }, 14 - fetch::Fetcher, 15 }; 16 17 fn handle_submit( 18 - full_route: Route, 19 fetcher: Fetcher, 20 mut error: Signal<Option<String>>, 21 mut is_loading: Signal<bool>, ··· 34 #[cfg(target_arch = "wasm32")] 35 { 36 use gloo_storage::Storage; 37 - gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); 38 spawn(async move { 39 match start_oauth_flow(handle, fetcher).await { 40 Ok(_) => { ··· 51 } 52 53 #[component] 54 - pub fn LoginModal(open: Signal<bool>) -> Element { 55 let mut handle_input = use_signal(|| String::new()); 56 let error = use_signal(|| Option::<String>::None); 57 let is_loading = use_signal(|| false); 58 - let full_route = use_route::<Route>(); 59 let fetcher = use_context::<Fetcher>(); 60 - let submit_route = full_route.clone(); 61 let submit_fetcher = fetcher.clone(); 62 let submit_closure1 = move || { 63 - let submit_route = submit_route.clone(); 64 let submit_fetcher = submit_fetcher.clone(); 65 handle_submit( 66 - submit_route, 67 submit_fetcher, 68 error, 69 is_loading, ··· 73 }; 74 75 let submit_closure2 = move || { 76 - let submit_route = full_route.clone(); 77 let submit_fetcher = fetcher.clone(); 78 handle_submit( 79 - submit_route, 80 submit_fetcher, 81 error, 82 is_loading,
··· 4 use jacquard::oauth::session::ClientData; 5 use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr}; 6 7 + use crate::components::{ 8 + button::{Button, ButtonVariant}, 9 + dialog::{DialogContent, DialogRoot, DialogTitle}, 10 + input::Input, 11 }; 12 + use crate::fetch::Fetcher; 13 + use crate::CONFIG; 14 15 fn handle_submit( 16 + cached_route: String, 17 fetcher: Fetcher, 18 mut error: Signal<Option<String>>, 19 mut is_loading: Signal<bool>, ··· 32 #[cfg(target_arch = "wasm32")] 33 { 34 use gloo_storage::Storage; 35 + gloo_storage::LocalStorage::set("cached_route", cached_route).ok(); 36 spawn(async move { 37 match start_oauth_flow(handle, fetcher).await { 38 Ok(_) => { ··· 49 } 50 51 #[component] 52 + pub fn LoginModal(open: Signal<bool>, cached_route: String) -> Element { 53 let mut handle_input = use_signal(|| String::new()); 54 let error = use_signal(|| Option::<String>::None); 55 let is_loading = use_signal(|| false); 56 let fetcher = use_context::<Fetcher>(); 57 + let cached_route_clone = cached_route.clone(); 58 let submit_fetcher = fetcher.clone(); 59 let submit_closure1 = move || { 60 + let cached_route = cached_route_clone.clone(); 61 let submit_fetcher = submit_fetcher.clone(); 62 handle_submit( 63 + cached_route, 64 submit_fetcher, 65 error, 66 is_loading, ··· 70 }; 71 72 let submit_closure2 = move || { 73 + let cached_route = cached_route.clone(); 74 let submit_fetcher = fetcher.clone(); 75 handle_submit( 76 + cached_route, 77 submit_fetcher, 78 error, 79 is_loading,
+4 -11
crates/weaver-app/src/components/profile.rs
··· 2 3 use std::sync::Arc; 4 5 - use crate::Route; 6 use crate::components::button::{Button, ButtonVariant}; 7 use crate::components::collab::api::{ReceivedInvite, accept_invite, fetch_received_invites}; 8 use crate::components::{ 9 BskyIcon, TangledIcon, 10 avatar::{Avatar, AvatarImage}, 11 }; 12 use crate::fetch::Fetcher; 13 use dioxus::prelude::*; 14 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 406 use gloo_timers::future::TimeoutFuture; 407 TimeoutFuture::new(500).await; 408 } 409 - // Navigate to the entry - parse AT-URI into path segments 410 - // at://did/collection/rkey -> ["did", "collection", "rkey"] 411 - let uri_str = resource_uri_nav.to_string(); 412 - let uri_parts: Vec<String> = uri_str 413 - .strip_prefix("at://") 414 - .unwrap_or(&uri_str) 415 - .split('/') 416 - .map(|s| s.to_string()) 417 - .collect(); 418 - nav.push(Route::RecordPage { uri: uri_parts }); 419 } 420 Err(e) => { 421 error.set(Some(format!("Failed: {}", e)));
··· 2 3 use std::sync::Arc; 4 5 use crate::components::button::{Button, ButtonVariant}; 6 use crate::components::collab::api::{ReceivedInvite, accept_invite, fetch_received_invites}; 7 use crate::components::{ 8 BskyIcon, TangledIcon, 9 avatar::{Avatar, AvatarImage}, 10 }; 11 + use crate::env::WEAVER_APP_HOST; 12 use crate::fetch::Fetcher; 13 use dioxus::prelude::*; 14 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 406 use gloo_timers::future::TimeoutFuture; 407 TimeoutFuture::new(500).await; 408 } 409 + // Navigate to record page on main domain 410 + let url = format!("{}/record/{}", WEAVER_APP_HOST, resource_uri_nav); 411 + nav.push(url); 412 } 413 Err(e) => { 414 error.set(Some(format!("Failed: {}", e)));
+16 -16
crates/weaver-app/src/components/profile_actions.rs
··· 1 //! Actions sidebar/menubar for profile page. 2 3 - use crate::Route; 4 use crate::auth::AuthState; 5 use crate::components::button::{Button, ButtonVariant}; 6 use dioxus::prelude::*; 7 use jacquard::types::ident::AtIdentifier; ··· 32 aside { class: "profile-actions", 33 div { class: "profile-actions-container", 34 div { class: "profile-actions-list", 35 - Link { 36 - to: Route::NewDraft { ident: ident(), notebook: None }, 37 - class: "profile-action-link", 38 Button { 39 variant: ButtonVariant::Outline, 40 "New Entry" ··· 48 "New Notebook" 49 } 50 51 - Link { 52 - to: Route::DraftsList { ident: ident() }, 53 - class: "profile-action-link", 54 Button { 55 variant: ButtonVariant::Ghost, 56 "Drafts" 57 } 58 } 59 60 - Link { 61 - to: Route::InvitesPage { ident: ident() }, 62 - class: "profile-action-link", 63 Button { 64 variant: ButtonVariant::Ghost, 65 "Invites" ··· 90 91 rsx! { 92 div { class: "profile-actions-menubar", 93 - Link { 94 - to: Route::NewDraft { ident: ident(), notebook: None }, 95 Button { 96 variant: ButtonVariant::Primary, 97 "New Entry" 98 } 99 } 100 101 - Link { 102 - to: Route::DraftsList { ident: ident() }, 103 Button { 104 variant: ButtonVariant::Ghost, 105 "Drafts" 106 } 107 } 108 109 - Link { 110 - to: Route::InvitesPage { ident: ident() }, 111 Button { 112 variant: ButtonVariant::Ghost, 113 "Invites"
··· 1 //! Actions sidebar/menubar for profile page. 2 3 use crate::auth::AuthState; 4 + use crate::components::app_link::{AppLink, AppLinkTarget}; 5 use crate::components::button::{Button, ButtonVariant}; 6 use dioxus::prelude::*; 7 use jacquard::types::ident::AtIdentifier; ··· 32 aside { class: "profile-actions", 33 div { class: "profile-actions-container", 34 div { class: "profile-actions-list", 35 + AppLink { 36 + to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, 37 + class: "profile-action-link".to_string(), 38 Button { 39 variant: ButtonVariant::Outline, 40 "New Entry" ··· 48 "New Notebook" 49 } 50 51 + AppLink { 52 + to: AppLinkTarget::Drafts { ident: ident() }, 53 + class: "profile-action-link".to_string(), 54 Button { 55 variant: ButtonVariant::Ghost, 56 "Drafts" 57 } 58 } 59 60 + AppLink { 61 + to: AppLinkTarget::Invites { ident: ident() }, 62 + class: "profile-action-link".to_string(), 63 Button { 64 variant: ButtonVariant::Ghost, 65 "Invites" ··· 90 91 rsx! { 92 div { class: "profile-actions-menubar", 93 + AppLink { 94 + to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, 95 Button { 96 variant: ButtonVariant::Primary, 97 "New Entry" 98 } 99 } 100 101 + AppLink { 102 + to: AppLinkTarget::Drafts { ident: ident() }, 103 Button { 104 variant: ButtonVariant::Ghost, 105 "Drafts" 106 } 107 } 108 109 + AppLink { 110 + to: AppLinkTarget::Invites { ident: ident() }, 111 Button { 112 variant: ButtonVariant::Ghost, 113 "Invites"
+34 -6
crates/weaver-app/src/data.rs
··· 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 rkey: ReadSignal<SmolStr>, 1587 ) -> ( 1588 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1589 Memo<Option<crate::fetch::LeafletDocumentData>>, 1590 ) { 1591 use weaver_api::pub_leaflet::document::Document; ··· 1661 for page in &record.value.pages { 1662 match page { 1663 DocumentPagesItem::LinearDocument(linear_doc) => { 1664 - html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1665 } 1666 DocumentPagesItem::Canvas(_) => { 1667 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1668 } 1669 DocumentPagesItem::Unknown(_) => { 1670 - html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1671 } 1672 } 1673 } ··· 1773 for page in &record.value.pages { 1774 match page { 1775 DocumentPagesItem::LinearDocument(linear_doc) => { 1776 - html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1777 } 1778 DocumentPagesItem::Canvas(_) => { 1779 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1780 } 1781 DocumentPagesItem::Unknown(_) => { 1782 - html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1783 } 1784 } 1785 } ··· 1810 ident: ReadSignal<AtIdentifier<'static>>, 1811 rkey: ReadSignal<SmolStr>, 1812 ) -> ( 1813 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1814 Memo<Option<crate::fetch::PcktDocumentData>>, 1815 ) { 1816 let fetcher = use_context::<crate::fetch::Fetcher>();
··· 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 rkey: ReadSignal<SmolStr>, 1587 ) -> ( 1588 + Result< 1589 + Resource< 1590 + Option<( 1591 + serde_json::Value, 1592 + serde_json::Value, 1593 + Option<String>, 1594 + Option<String>, 1595 + )>, 1596 + >, 1597 + RenderError, 1598 + >, 1599 Memo<Option<crate::fetch::LeafletDocumentData>>, 1600 ) { 1601 use weaver_api::pub_leaflet::document::Document; ··· 1671 for page in &record.value.pages { 1672 match page { 1673 DocumentPagesItem::LinearDocument(linear_doc) => { 1674 + html.push_str( 1675 + &render_linear_document(linear_doc, &ctx, &fetcher).await, 1676 + ); 1677 } 1678 DocumentPagesItem::Canvas(_) => { 1679 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1680 } 1681 DocumentPagesItem::Unknown(_) => { 1682 + html.push_str( 1683 + "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1684 + ); 1685 } 1686 } 1687 } ··· 1787 for page in &record.value.pages { 1788 match page { 1789 DocumentPagesItem::LinearDocument(linear_doc) => { 1790 + html.push_str( 1791 + &render_linear_document(linear_doc, &ctx, &fetcher).await, 1792 + ); 1793 } 1794 DocumentPagesItem::Canvas(_) => { 1795 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1796 } 1797 DocumentPagesItem::Unknown(_) => { 1798 + html.push_str( 1799 + "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1800 + ); 1801 } 1802 } 1803 } ··· 1828 ident: ReadSignal<AtIdentifier<'static>>, 1829 rkey: ReadSignal<SmolStr>, 1830 ) -> ( 1831 + Result< 1832 + Resource< 1833 + Option<( 1834 + serde_json::Value, 1835 + serde_json::Value, 1836 + Option<String>, 1837 + Option<String>, 1838 + )>, 1839 + >, 1840 + RenderError, 1841 + >, 1842 Memo<Option<crate::fetch::PcktDocumentData>>, 1843 ) { 1844 let fetcher = use_context::<crate::fetch::Fetcher>();
+13 -10
crates/weaver-app/src/lib.rs
··· 72 #[layout(RecordIndex)] 73 #[route("/:..uri")] 74 RecordPage { uri: Vec<String> }, 75 - #[end_layout] 76 #[end_nest] 77 #[route("/callback?:state&:iss&:code")] 78 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, ··· 124 #[route("/e/:rkey")] 125 NotebookEntryByRkey { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 126 #[route("/e/:rkey/edit")] 127 - NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr } 128 - 129 } 130 131 pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config { ··· 170 tracing::warn!("Host header not valid UTF-8"); 171 return None; 172 }; 173 - tracing::info!(host, "Subdomain detection: got host"); 174 175 let host_str = host.split(':').next().unwrap_or(host); 176 let Some(subdomain) = extract_subdomain(host_str, WEAVER_APP_DOMAIN) else { 177 - tracing::info!(host_str, domain = WEAVER_APP_DOMAIN, "Not a subdomain request"); 178 return None; 179 }; 180 - tracing::info!(subdomain, "Subdomain detection: extracted subdomain"); 181 - 182 // Look up notebook by global path 183 let result = lookup_subdomain_context(&fetcher, &subdomain).await; 184 if result.is_none() { ··· 192 #[cfg(feature = "fullstack-server")] 193 let ctx = match &*ctx_resource.read() { 194 Some(ctx) => ctx.clone(), 195 - None => return rsx! { div { "Loading..." } }, 196 }; 197 198 #[cfg(not(feature = "fullstack-server"))] 199 - let ctx = None::<SubdomainContext>; 200 201 let auth_state = use_signal(|| AuthState::default()); 202 #[allow(unused)] ··· 235 }); 236 } 237 238 - #[cfg(all(target_family = "wasm", target_os = "unknown"))] 239 use_context_provider(|| restore_result); 240 241 if sub == LinkMode::Subdomain { 242 use_context_provider(|| ctx.unwrap()); 243 rsx! { 244 SubdomainApp {} 245 } 246 } else { 247 rsx! { 248 MainDomainApp {} 249 }
··· 72 #[layout(RecordIndex)] 73 #[route("/:..uri")] 74 RecordPage { uri: Vec<String> }, 75 + #[end_layout] 76 #[end_nest] 77 #[route("/callback?:state&:iss&:code")] 78 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, ··· 124 #[route("/e/:rkey")] 125 NotebookEntryByRkey { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 126 #[route("/e/:rkey/edit")] 127 + NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 128 } 129 130 pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config { ··· 169 tracing::warn!("Host header not valid UTF-8"); 170 return None; 171 }; 172 173 let host_str = host.split(':').next().unwrap_or(host); 174 let Some(subdomain) = extract_subdomain(host_str, WEAVER_APP_DOMAIN) else { 175 + tracing::info!( 176 + host_str, 177 + domain = WEAVER_APP_DOMAIN, 178 + "Not a subdomain request" 179 + ); 180 return None; 181 }; 182 // Look up notebook by global path 183 let result = lookup_subdomain_context(&fetcher, &subdomain).await; 184 if result.is_none() { ··· 192 #[cfg(feature = "fullstack-server")] 193 let ctx = match &*ctx_resource.read() { 194 Some(ctx) => ctx.clone(), 195 + None => { 196 + return rsx! { div { "Loading..." } }; 197 + } 198 }; 199 200 #[cfg(not(feature = "fullstack-server"))] 201 + let ctx = { None::<SubdomainContext> }; 202 203 let auth_state = use_signal(|| AuthState::default()); 204 #[allow(unused)] ··· 237 }); 238 } 239 240 use_context_provider(|| restore_result); 241 242 if sub == LinkMode::Subdomain { 243 + tracing::info!("App: rendering SubdomainApp"); 244 use_context_provider(|| ctx.unwrap()); 245 rsx! { 246 SubdomainApp {} 247 } 248 } else { 249 + tracing::info!("App: rendering MainDomainApp"); 250 rsx! { 251 MainDomainApp {} 252 }
+4 -11
crates/weaver-app/src/main.rs
··· 1 //! Weaver App main binary. 2 3 use dioxus::prelude::*; 4 - 5 #[cfg(feature = "server")] 6 use std::sync::Arc; 7 - 8 #[cfg(feature = "server")] 9 use tower::Service; 10 - 11 - #[cfg(feature = "server")] 12 use weaver_app::{App, CONFIG, SubdomainApp, SubdomainContext, fetch}; 13 - 14 - #[cfg(not(feature = "server"))] 15 - use weaver_app::{App, SubdomainApp}; 16 - 17 - #[cfg(target_arch = "wasm32")] 18 - use lol_alloc::{FreeListAllocator, LockedAllocator}; 19 - 20 #[cfg(target_arch = "wasm32")] 21 #[global_allocator] 22 static ALLOCATOR: LockedAllocator<FreeListAllocator> =
··· 1 //! Weaver App main binary. 2 3 + #[allow(unused)] 4 use dioxus::prelude::*; 5 + #[cfg(target_arch = "wasm32")] 6 + use lol_alloc::{FreeListAllocator, LockedAllocator}; 7 #[cfg(feature = "server")] 8 use std::sync::Arc; 9 #[cfg(feature = "server")] 10 use tower::Service; 11 + #[allow(unused)] 12 use weaver_app::{App, CONFIG, SubdomainApp, SubdomainContext, fetch}; 13 #[cfg(target_arch = "wasm32")] 14 #[global_allocator] 15 static ALLOCATOR: LockedAllocator<FreeListAllocator> =
+28 -23
crates/weaver-app/src/subdomain_app.rs
··· 3 //! Separate router for subdomain hosting with simpler route structure. 4 5 use dioxus::prelude::*; 6 - use jacquard::oauth::client::OAuthClient; 7 - use jacquard::oauth::session::ClientData; 8 use jacquard::smol_str::{SmolStr, ToSmolStr}; 9 use jacquard::types::string::AtIdentifier; 10 11 - use crate::auth; 12 - use crate::auth::{AuthState, AuthStore}; 13 use crate::components::identity::RepositoryIndex; 14 use crate::components::{EntryPage, NotebookCss}; 15 - use crate::host_mode::{LinkMode, SubdomainContext}; 16 use crate::views::{NotebookEntryByRkey, NotebookEntryEdit, NotebookIndex, SubdomainNavbar}; 17 - use crate::{CONFIG, fetch}; 18 19 /// Subdomain route enum - simpler paths without /:ident/:notebook prefix. 20 #[derive(Debug, Clone, Routable, PartialEq)] ··· 54 let request = ResolveGlobalNotebook::new().path(path).build(); 55 56 match fetcher.send(request).await { 57 - Ok(response) => { 58 - let output = response.into_output().ok()?; 59 - let notebook = output.notebook; 60 61 - let owner = notebook.uri.authority().clone().into_static(); 62 - let rkey = notebook.uri.rkey()?.0.to_smolstr(); 63 - let notebook_path = notebook 64 - .path 65 - .map(|p| SmolStr::new(p.as_ref())) 66 - .unwrap_or_else(|| SmolStr::new(path)); 67 68 - Some(SubdomainContext { 69 - owner, 70 - notebook_path, 71 - notebook_rkey: rkey, 72 - notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(), 73 - }) 74 - } 75 Err(e) => { 76 - tracing::debug!(path = path, error = %e, "Global notebook lookup failed"); 77 None 78 } 79 }
··· 3 //! Separate router for subdomain hosting with simpler route structure. 4 5 use dioxus::prelude::*; 6 use jacquard::smol_str::{SmolStr, ToSmolStr}; 7 use jacquard::types::string::AtIdentifier; 8 9 use crate::components::identity::RepositoryIndex; 10 use crate::components::{EntryPage, NotebookCss}; 11 + use crate::host_mode::SubdomainContext; 12 use crate::views::{NotebookEntryByRkey, NotebookEntryEdit, NotebookIndex, SubdomainNavbar}; 13 14 /// Subdomain route enum - simpler paths without /:ident/:notebook prefix. 15 #[derive(Debug, Clone, Routable, PartialEq)] ··· 49 let request = ResolveGlobalNotebook::new().path(path).build(); 50 51 match fetcher.send(request).await { 52 + Ok(response) => match response.into_output() { 53 + Ok(output) => { 54 + let notebook = output.notebook; 55 56 + let owner = notebook.uri.authority().clone().into_static(); 57 + let Some(rkey) = notebook.uri.rkey() else { 58 + tracing::warn!(path, uri = %notebook.uri, "Notebook URI missing rkey"); 59 + return None; 60 + }; 61 + let rkey = rkey.0.to_smolstr(); 62 + let notebook_path = notebook 63 + .path 64 + .map(|p| SmolStr::new(p.as_ref())) 65 + .unwrap_or_else(|| SmolStr::new(path)); 66 67 + tracing::info!(path, %owner, %rkey, "Notebook lookup succeeded"); 68 + Some(SubdomainContext { 69 + owner, 70 + notebook_path, 71 + notebook_rkey: rkey, 72 + notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(), 73 + }) 74 + } 75 + Err(e) => { 76 + tracing::warn!(path, error = %e, "Failed to parse notebook response"); 77 + None 78 + } 79 + }, 80 Err(e) => { 81 + tracing::warn!(path, error = %e, "Global notebook lookup request failed"); 82 None 83 } 84 }
+11 -5
crates/weaver-app/src/views/footer.rs
··· 12 13 /// Determines if the current route should show the full footer or just the minimal version. 14 /// Full footer shows on shell pages (Home, Editor) and on owner's content pages. 15 - fn should_show_full_footer(route: &Route) -> bool { 16 match route { 17 // Shell pages: always show full footer 18 Route::Home {} ··· 67 } 68 69 #[component] 70 - pub fn Footer() -> Element { 71 - let route = use_route::<Route>(); 72 - let show_full = should_show_full_footer(&route); 73 - 74 rsx! { 75 document::Link { rel: "stylesheet", href: FOOTER_CSS } 76 ··· 174 rel: "noopener", 175 "Report Bug" 176 } 177 } 178 } 179 }
··· 12 13 /// Determines if the current route should show the full footer or just the minimal version. 14 /// Full footer shows on shell pages (Home, Editor) and on owner's content pages. 15 + pub fn should_show_full_footer(route: &Route) -> bool { 16 match route { 17 // Shell pages: always show full footer 18 Route::Home {} ··· 67 } 68 69 #[component] 70 + pub fn Footer(#[props(default = true)] show_full: bool) -> Element { 71 rsx! { 72 document::Link { rel: "stylesheet", href: FOOTER_CSS } 73 ··· 171 rel: "noopener", 172 "Report Bug" 173 } 174 + 175 + span { class: "footer-separator", "|" } 176 + 177 + Link { 178 + to: crate::env::WEAVER_APP_HOST, 179 + class: "footer-link", 180 + "weaver.sh" 181 + } 182 + 183 } 184 } 185 }
+1 -1
crates/weaver-app/src/views/mod.rs
··· 39 pub use invites::InvitesPage; 40 41 mod footer; 42 - pub use footer::Footer; 43 44 mod static_page; 45 pub use static_page::{AboutPage, PrivacyPage, TermsPage};
··· 39 pub use invites::InvitesPage; 40 41 mod footer; 42 + pub use footer::{Footer, should_show_full_footer}; 43 44 mod static_page; 45 pub use static_page::{AboutPage, PrivacyPage, TermsPage};
+5 -3
crates/weaver-app/src/views/navbar.rs
··· 4 use crate::components::login::LoginModal; 5 use crate::data::{use_get_handle, use_load_handle}; 6 use crate::fetch::Fetcher; 7 - use crate::views::Footer; 8 use dioxus::prelude::*; 9 use dioxus_primitives::toast::{ToastOptions, use_toast}; 10 use jacquard::types::ident::AtIdentifier; ··· 62 #[cfg(feature = "fullstack-server")] 63 route_handle_res?; 64 65 let fetcher = use_context::<Fetcher>(); 66 let mut show_login_modal = use_signal(|| false); 67 ··· 292 293 } 294 LoginModal { 295 - open: show_login_modal 296 } 297 } 298 } ··· 301 Outlet::<Route> {} 302 } 303 304 - Footer {} 305 } 306 } 307 }
··· 4 use crate::components::login::LoginModal; 5 use crate::data::{use_get_handle, use_load_handle}; 6 use crate::fetch::Fetcher; 7 + use crate::views::{Footer, should_show_full_footer}; 8 use dioxus::prelude::*; 9 use dioxus_primitives::toast::{ToastOptions, use_toast}; 10 use jacquard::types::ident::AtIdentifier; ··· 62 #[cfg(feature = "fullstack-server")] 63 route_handle_res?; 64 65 + #[allow(unused)] 66 let fetcher = use_context::<Fetcher>(); 67 let mut show_login_modal = use_signal(|| false); 68 ··· 293 294 } 295 LoginModal { 296 + open: show_login_modal, 297 + cached_route: format!("{}", route), 298 } 299 } 300 } ··· 303 Outlet::<Route> {} 304 } 305 306 + Footer { show_full: should_show_full_footer(&route) } 307 } 308 } 309 }
+39 -45
crates/weaver-app/src/views/subdomain_navbar.rs
··· 4 use jacquard::types::string::{AtIdentifier, Did}; 5 6 use crate::SubdomainRoute; 7 use crate::auth::{AuthState, RestoreResult}; 8 use crate::components::button::{Button, ButtonVariant}; 9 use crate::components::login::LoginModal; 10 use crate::data::{use_get_handle, use_handle}; 11 use crate::fetch::Fetcher; 12 use crate::host_mode::SubdomainContext; 13 use crate::views::Footer; ··· 26 let ctx = use_context::<SubdomainContext>(); 27 let route = use_route::<SubdomainRoute>(); 28 let auth_state = use_context::<Signal<AuthState>>(); 29 let fetcher = use_context::<Fetcher>(); 30 let mut show_login_modal = use_signal(|| false); 31 ··· 61 div { 62 id: "navbar", 63 nav { class: "breadcrumbs", 64 - // Notebook title links to index 65 - Link { 66 - to: SubdomainRoute::SubdomainLanding {}, 67 - class: "breadcrumb", 68 - "{ctx.notebook_title}" 69 - } 70 - 71 - // Show current location breadcrumb based on route 72 - match &route { 73 - SubdomainRoute::SubdomainLanding {} | SubdomainRoute::SubdomainIndexPage {} => { 74 - rsx! {} 75 - } 76 - SubdomainRoute::SubdomainEntry { title } => { 77 - rsx! { 78 - span { class: "breadcrumb-separator", " > " } 79 - span { class: "breadcrumb breadcrumb-current", "{title}" } 80 - } 81 } 82 - SubdomainRoute::SubdomainEntryByRkey { rkey } => { 83 - rsx! { 84 - span { class: "breadcrumb-separator", " > " } 85 - span { class: "breadcrumb breadcrumb-current", "{rkey}" } 86 } 87 - } 88 - SubdomainRoute::SubdomainEntryEdit { rkey } => { 89 - rsx! { 90 - span { class: "breadcrumb-separator", " > " } 91 - Link { 92 - to: SubdomainRoute::SubdomainEntryByRkey { rkey: rkey.clone() }, 93 - class: "breadcrumb", 94 - "{rkey}" 95 } 96 - span { class: "breadcrumb-separator", " > " } 97 - span { class: "breadcrumb breadcrumb-current", "Edit" } 98 } 99 - } 100 - SubdomainRoute::SubdomainProfile { ident } => { 101 - rsx! { 102 - span { class: "breadcrumb-separator", " > " } 103 - span { class: "breadcrumb breadcrumb-current", "@{ident}" } 104 } 105 } 106 } 107 - } 108 - 109 - // Author profile link 110 - nav { class: "nav-tools", 111 - AuthorProfileLink { ident: ctx.owner.clone() } 112 - } 113 114 // Auth button 115 if auth_state.read().is_authenticated() { ··· 126 } 127 } 128 LoginModal { 129 - open: show_login_modal 130 } 131 } 132 } 133 134 main { class: "app-main", 135 Outlet::<SubdomainRoute> {} 136 } 137 138 - Footer {} 139 } 140 } 141 } ··· 189 let (handle_res, handle) = use_handle(ident); 190 191 #[cfg(feature = "fullstack-server")] 192 - let mut handle_res = handle_res?; 193 194 rsx! { 195 Link {
··· 4 use jacquard::types::string::{AtIdentifier, Did}; 5 6 use crate::SubdomainRoute; 7 + #[allow(unused_imports)] 8 use crate::auth::{AuthState, RestoreResult}; 9 use crate::components::button::{Button, ButtonVariant}; 10 use crate::components::login::LoginModal; 11 use crate::data::{use_get_handle, use_handle}; 12 + #[allow(unused_imports)] 13 + use crate::env::WEAVER_APP_HOST; 14 use crate::fetch::Fetcher; 15 use crate::host_mode::SubdomainContext; 16 use crate::views::Footer; ··· 29 let ctx = use_context::<SubdomainContext>(); 30 let route = use_route::<SubdomainRoute>(); 31 let auth_state = use_context::<Signal<AuthState>>(); 32 + 33 + #[allow(unused)] 34 let fetcher = use_context::<Fetcher>(); 35 let mut show_login_modal = use_signal(|| false); 36 ··· 66 div { 67 id: "navbar", 68 nav { class: "breadcrumbs", 69 + // Notebook title links to index 70 + Link { 71 + to: SubdomainRoute::SubdomainLanding {}, 72 + class: "breadcrumb", 73 + "{ctx.notebook_title}" 74 } 75 + 76 + // Show current location breadcrumb based on route 77 + match &route { 78 + SubdomainRoute::SubdomainLanding {} | SubdomainRoute::SubdomainIndexPage {} | SubdomainRoute::SubdomainEntryByRkey { .. } | SubdomainRoute::SubdomainEntry { .. } => { 79 + rsx! {} 80 } 81 + SubdomainRoute::SubdomainEntryEdit { rkey } => { 82 + rsx! { 83 + span { class: "breadcrumb-separator", " > " } 84 + Link { 85 + to: SubdomainRoute::SubdomainEntryByRkey { rkey: rkey.clone() }, 86 + class: "breadcrumb", 87 + "{rkey}" 88 + } 89 } 90 } 91 + SubdomainRoute::SubdomainProfile { ident } => { 92 + rsx! { 93 + span { class: "breadcrumb-separator", " > " } 94 + span { class: "breadcrumb breadcrumb-current", "@{ident}" } 95 + } 96 } 97 } 98 } 99 + // Author profile link 100 + nav { class: "nav-tools", 101 + AuthorProfileLink { ident: ctx.owner.clone() } 102 + } 103 104 // Auth button 105 if auth_state.read().is_authenticated() { ··· 116 } 117 } 118 LoginModal { 119 + open: show_login_modal, 120 + cached_route: format!("{}", route), 121 } 122 } 123 } 124 125 + 126 + 127 + 128 main { class: "app-main", 129 Outlet::<SubdomainRoute> {} 130 } 131 132 + Footer { show_full: false } 133 } 134 } 135 } ··· 183 let (handle_res, handle) = use_handle(ident); 184 185 #[cfg(feature = "fullstack-server")] 186 + handle_res?; 187 188 rsx! { 189 Link {
+1 -1
crates/weaver-index/migrations/clickhouse/009_handle_mappings_identity_mv.sql
··· 8 'active' as account_status, 9 'identity' as source, 10 event_time, 11 - now64(3) as indexed_at 12 FROM raw_identity_events 13 WHERE handle != ''
··· 8 'active' as account_status, 9 'identity' as source, 10 event_time, 11 + indexed_at 12 FROM raw_identity_events 13 WHERE handle != ''
+1 -1
crates/weaver-index/migrations/clickhouse/010_handle_mappings_account_mv.sql
··· 8 a.status as account_status, 9 'account' as source, 10 a.event_time, 11 - now64(3) as indexed_at 12 FROM raw_account_events a 13 INNER JOIN handle_mappings h ON h.did = a.did AND h.freed = 0 14 WHERE a.active = 0 AND a.status != ''
··· 8 a.status as account_status, 9 'account' as source, 10 a.event_time, 11 + a.indexed_at 12 FROM raw_account_events a 13 INNER JOIN handle_mappings h ON h.did = a.did AND h.freed = 0 14 WHERE a.active = 0 AND a.status != ''
+1 -1
crates/weaver-index/migrations/clickhouse/012_profiles_weaver_mv.sql
··· 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 event_time, 13 - now64(3) as indexed_at, 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 FROM raw_records 16 WHERE collection = 'sh.weaver.actor.profile'
··· 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 event_time, 13 + indexed_at, 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 FROM raw_records 16 WHERE collection = 'sh.weaver.actor.profile'
+1 -1
crates/weaver-index/migrations/clickhouse/014_profiles_bsky_mv.sql
··· 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 event_time, 13 - now64(3) as indexed_at, 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 FROM raw_records 16 WHERE collection = 'app.bsky.actor.profile'
··· 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 event_time, 13 + indexed_at, 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 FROM raw_records 16 WHERE collection = 'app.bsky.actor.profile'
+1 -1
crates/weaver-index/migrations/clickhouse/018_notebooks_mv.sql
··· 13 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 14 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 15 event_time, 16 - now64(3) as indexed_at, 17 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 18 record 19 FROM raw_records
··· 13 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 14 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 15 event_time, 16 + indexed_at, 17 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 18 record 19 FROM raw_records
+1 -1
crates/weaver-index/migrations/clickhouse/021_entries_mv.sql
··· 12 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 13 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 14 event_time, 15 - now64(3) as indexed_at, 16 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 17 record 18 FROM raw_records
··· 12 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 13 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 14 event_time, 15 + indexed_at, 16 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 17 record 18 FROM raw_records
+1 -1
crates/weaver-index/migrations/clickhouse/024_drafts_mv.sql
··· 7 cid, 8 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 9 event_time, 10 - now64(3) as indexed_at, 11 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 12 FROM raw_records 13 WHERE collection = 'sh.weaver.edit.draft'
··· 7 cid, 8 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 9 event_time, 10 + indexed_at, 11 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 12 FROM raw_records 13 WHERE collection = 'sh.weaver.edit.draft'
+1 -1
crates/weaver-index/migrations/clickhouse/026_edit_roots_mv.sql
··· 60 61 event_time as created_at, 62 event_time, 63 - now64(3) as indexed_at, 64 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 65 FROM raw_records 66 WHERE collection = 'sh.weaver.edit.root'
··· 60 61 event_time as created_at, 62 event_time, 63 + indexed_at, 64 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 65 FROM raw_records 66 WHERE collection = 'sh.weaver.edit.root'
+1 -1
crates/weaver-index/migrations/clickhouse/027_edit_diffs_mv.sql
··· 68 69 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 70 event_time, 71 - now64(3) as indexed_at, 72 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 73 FROM raw_records 74 WHERE collection = 'sh.weaver.edit.diff'
··· 68 69 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 70 event_time, 71 + indexed_at, 72 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 73 FROM raw_records 74 WHERE collection = 'sh.weaver.edit.diff'
+1 -1
crates/weaver-index/migrations/clickhouse/030_collab_invites_mv.sql
··· 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 19 event_time, 20 - now64(3) as indexed_at, 21 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 22 FROM raw_records 23 WHERE collection = 'sh.weaver.collab.invite'
··· 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 19 event_time, 20 + indexed_at, 21 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 22 FROM raw_records 23 WHERE collection = 'sh.weaver.collab.invite'
+1 -1
crates/weaver-index/migrations/clickhouse/032_collab_accepts_mv.sql
··· 18 19 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 20 event_time, 21 - now64(3) as indexed_at, 22 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 23 FROM raw_records 24 WHERE collection = 'sh.weaver.collab.accept'
··· 18 19 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 20 event_time, 21 + indexed_at, 22 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 23 FROM raw_records 24 WHERE collection = 'sh.weaver.collab.accept'
+1 -1
crates/weaver-index/migrations/clickhouse/034_collab_sessions_mv.sql
··· 16 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 event_time, 19 - now64(3) as indexed_at, 20 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 21 FROM raw_records 22 WHERE collection = 'sh.weaver.collab.session'
··· 16 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 event_time, 19 + indexed_at, 20 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 21 FROM raw_records 22 WHERE collection = 'sh.weaver.collab.session'
+1 -1
crates/weaver-index/migrations/clickhouse/039_notebook_entries_mv.sql
··· 14 -- Position from array index 15 assumeNotNull(entry_position) as position, 16 17 - now64(3) as indexed_at 18 FROM notebooks 19 ARRAY JOIN 20 record.entryList[].uri as entry_uri,
··· 14 -- Position from array index 15 assumeNotNull(entry_position) as position, 16 17 + indexed_at 18 FROM notebooks 19 ARRAY JOIN 20 record.entryList[].uri as entry_uri,
+2 -2
docker-compose.yml
··· 54 #TAP_FULL_NETWORK: true 55 #TAP_SIGNAL_COLLECTION: place.stream.chat.profile 56 TAP_SIGNAL_COLLECTION: sh.weaver.actor.profile 57 - TAP_COLLECTION_FILTERS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*" 58 healthcheck: 59 test: ["CMD", "wget", "-q", "--spider", "http://localhost:2480/health"] 60 interval: 20s ··· 81 TAP_URL: ws://tap:2480/channel 82 TAP_SEND_ACKS: "true" 83 FIREHOSE_RELAY_URL: wss://bsky.network 84 - INDEXER_COLLECTIONS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*" 85 depends_on: 86 tap: 87 condition: service_healthy
··· 54 #TAP_FULL_NETWORK: true 55 #TAP_SIGNAL_COLLECTION: place.stream.chat.profile 56 TAP_SIGNAL_COLLECTION: sh.weaver.actor.profile 57 + TAP_COLLECTION_FILTERS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*,site.standard.*" 58 healthcheck: 59 test: ["CMD", "wget", "-q", "--spider", "http://localhost:2480/health"] 60 interval: 20s ··· 81 TAP_URL: ws://tap:2480/channel 82 TAP_SEND_ACKS: "true" 83 FIREHOSE_RELAY_URL: wss://bsky.network 84 + INDEXER_COLLECTIONS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*,site.standard.*" 85 depends_on: 86 tap: 87 condition: service_healthy