Compare commits

...

4 Commits

Author SHA1 Message Date
spinline
4231e0b3a7 Adjust sidebar layout to push profile section to the bottom and reduce avatar size
All checks were successful
Build MIPS Binary / build (push) Successful in 4m7s
2026-02-07 16:59:31 +03:00
spinline
1177412c87 Add user profile and logout button to sidebar footer
All checks were successful
Build MIPS Binary / build (push) Successful in 4m7s
2026-02-07 16:40:55 +03:00
spinline
aed753c64f Lower bcrypt cost to 6 to improve login speed on low-end hardware
All checks were successful
Build MIPS Binary / build (push) Successful in 4m8s
2026-02-07 16:24:06 +03:00
spinline
9d0eb11f16 Refactor App routing to fix infinite recursion and stack overflow errors
All checks were successful
Build MIPS Binary / build (push) Successful in 4m8s
2026-02-07 16:10:06 +03:00
5 changed files with 189 additions and 166 deletions

View File

@@ -67,7 +67,9 @@ pub async fn setup_handler(
} }
// 3. Create User // 3. Create User
let password_hash = match bcrypt::hash(&payload.password, bcrypt::DEFAULT_COST) { // Lower cost for faster login on low-power devices (MIPS routers etc.)
// Default is usually 12, which takes ~3s on slow CPUs. 6 should be much faster.
let password_hash = match bcrypt::hash(&payload.password, 6) {
Ok(h) => h, Ok(h) => h,
Err(e) => { Err(e) => {
tracing::error!("Failed to hash password: {}", e); tracing::error!("Failed to hash password: {}", e);

View File

@@ -1,6 +1,4 @@
use crate::components::layout::sidebar::Sidebar; use crate::components::layout::protected::Protected;
use crate::components::layout::statusbar::StatusBar;
use crate::components::layout::toolbar::Toolbar;
use crate::components::toast::ToastContainer; use crate::components::toast::ToastContainer;
use crate::components::torrent::table::TorrentTable; use crate::components::torrent::table::TorrentTable;
use crate::components::auth::login::Login; use crate::components::auth::login::Login;
@@ -22,100 +20,69 @@ pub fn App() -> impl IntoView {
let (is_loading, set_is_loading) = create_signal(true); let (is_loading, set_is_loading) = create_signal(true);
let (is_authenticated, set_is_authenticated) = create_signal(false); let (is_authenticated, set_is_authenticated) = create_signal(false);
// Check Auth & Setup Status on load // Check Auth & Setup Status on load
create_effect(move |_| { create_effect(move |_| {
spawn_local(async move { spawn_local(async move {
logging::log!("App initialization started..."); logging::log!("App initialization started...");
// 1. Check Setup Status // 1. Check Setup Status
logging::log!("Checking setup status..."); let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await;
let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await;
match setup_res { match setup_res {
Ok(resp) => { Ok(resp) => {
if resp.ok() { if resp.ok() {
match resp.json::<SetupStatus>().await { match resp.json::<SetupStatus>().await {
Ok(status) => { Ok(status) => {
logging::log!("Setup status: completed={}", status.completed); if !status.completed {
if !status.completed { logging::log!("Setup not completed, redirecting to /setup");
logging::log!("Setup not completed, redirecting to /setup"); let navigate = use_navigate();
let navigate = use_navigate(); navigate("/setup", Default::default());
navigate("/setup", Default::default()); set_is_loading.set(false);
set_is_loading.set(false); return;
return;
}
} }
Err(e) => logging::error!("Failed to parse setup status: {}", e),
} }
} else { Err(e) => logging::error!("Failed to parse setup status: {}", e),
logging::error!("Setup status request failed: {}", resp.status());
} }
} }
Err(e) => logging::error!("Network error checking setup status: {}", e),
} }
Err(e) => logging::error!("Network error checking setup status: {}", e),
}
// 2. Check Auth Status // 2. Check Auth Status
logging::log!("Checking auth status..."); let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await;
let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await;
match auth_res { match auth_res {
Ok(resp) => { Ok(resp) => {
logging::log!("Auth check status: {}", resp.status()); if resp.status() == 200 {
if resp.status() == 200 { logging::log!("Authenticated!");
logging::log!("Authenticated!"); set_is_authenticated.set(true);
set_is_authenticated.set(true); } else {
} else { logging::log!("Not authenticated, redirecting to /login");
logging::log!("Not authenticated, checking if redirect needed"); let navigate = use_navigate();
let navigate = use_navigate(); let pathname = window().location().pathname().unwrap_or_default();
let pathname = window().location().pathname().unwrap_or_default(); if pathname != "/login" && pathname != "/setup" {
if pathname != "/login" && pathname != "/setup" { navigate("/login", Default::default());
navigate("/login", Default::default());
}
} }
} }
Err(e) => logging::error!("Network error checking auth status: {}", e),
} }
Err(e) => logging::error!("Network error checking auth status: {}", e),
}
logging::log!("App initialization finished, disabling loader."); set_is_loading.set(false);
set_is_loading.set(false);
});
}); });
// Initialize push notifications after user grants permission (Only if authenticated) });
// Initialize push notifications (Only if authenticated)
create_effect(move |_| { create_effect(move |_| {
if is_authenticated.get() { if is_authenticated.get() {
spawn_local(async { spawn_local(async {
// ... (Push notification logic kept same, shortened for brevity in this replace)
// Wait a bit for service worker to be ready // Wait a bit for service worker to be ready
gloo_timers::future::TimeoutFuture::new(2000).await; gloo_timers::future::TimeoutFuture::new(2000).await;
// Check if running on iOS and not standalone if crate::utils::platform::supports_push_notifications() && !crate::utils::platform::is_safari() {
if let Some(ios_message) = crate::utils::platform::get_ios_notification_info() { crate::store::subscribe_to_push_notifications().await;
log::warn!("iOS detected: {}", ios_message);
if let Some(store) = use_context::<crate::store::TorrentStore>() {
crate::store::show_toast_with_signal(
store.notifications,
shared::NotificationLevel::Info,
ios_message,
);
}
return;
} }
if !crate::utils::platform::supports_push_notifications() {
return;
}
if crate::utils::platform::is_safari() {
if let Some(store) = use_context::<crate::store::TorrentStore>() {
crate::store::show_toast_with_signal(
store.notifications,
shared::NotificationLevel::Info,
"Bildirim izni için sağ alttaki ayarlar ⚙️ ikonuna basın.".to_string(),
);
}
return;
}
crate::store::subscribe_to_push_notifications().await;
}); });
} }
}); });
@@ -127,38 +94,29 @@ pub fn App() -> impl IntoView {
<Route path="/login" view=move || view! { <Login /> } /> <Route path="/login" view=move || view! { <Login /> } />
<Route path="/setup" view=move || view! { <Setup /> } /> <Route path="/setup" view=move || view! { <Setup /> } />
<Route path="/*" view=move || { <Route path="/" view=move || {
view! { view! {
<Show when=move || !is_loading.get() fallback=|| view! { <Show when=move || !is_loading.get() fallback=|| view! {
<div class="flex items-center justify-center h-screen bg-base-100"> <div class="flex items-center justify-center h-screen bg-base-100">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
}> }>
<Show when=move || is_authenticated.get() fallback=|| view! { <Login /> }> <Show when=move || is_authenticated.get() fallback=|| ()>
// Protected Layout <Protected>
<div class="drawer lg:drawer-open h-full w-full"> <TorrentTable />
<input id="my-drawer" type="checkbox" class="drawer-toggle" /> </Protected>
</Show>
</Show>
}
}/>
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none"> <Route path="/settings" view=move || {
<Toolbar /> view! {
<Show when=move || !is_loading.get() fallback=|| ()>
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8"> <Show when=move || is_authenticated.get() fallback=|| ()>
<Routes> <Protected>
<Route path="/" view=move || view! { <TorrentTable /> } /> <div class="p-4">"Settings Page (Coming Soon)"</div>
<Route path="/settings" view=move || view! { <div class="p-4">"Settings Page (Coming Soon)"</div> } /> </Protected>
</Routes>
</main>
<StatusBar />
</div>
<div class="drawer-side z-40 transition-none duration-0">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay transition-none duration-0"></label>
<div class="menu p-0 min-h-full bg-base-200 text-base-content border-r border-base-300 transition-none duration-0">
<Sidebar />
</div>
</div>
</div>
</Show> </Show>
</Show> </Show>
} }

View File

@@ -1,3 +1,4 @@
pub mod sidebar; pub mod sidebar;
pub mod toolbar;
pub mod statusbar; pub mod statusbar;
pub mod toolbar;
pub mod protected;

View File

@@ -0,0 +1,30 @@
use leptos::*;
use crate::components::layout::sidebar::Sidebar;
use crate::components::layout::statusbar::StatusBar;
use crate::components::layout::toolbar::Toolbar;
#[component]
pub fn Protected(children: Children) -> impl IntoView {
view! {
<div class="drawer lg:drawer-open h-full w-full">
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none">
<Toolbar />
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8">
{children()}
</main>
<StatusBar />
</div>
<div class="drawer-side z-40 transition-none duration-0">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay transition-none duration-0"></label>
<div class="menu p-0 min-h-full bg-base-200 text-base-content border-r border-base-300 transition-none duration-0">
<Sidebar />
</div>
</div>
</div>
}
}

View File

@@ -74,69 +74,101 @@ pub fn Sidebar() -> impl IntoView {
} }
}; };
view! { let handle_logout = move |_| {
<div class="w-64 h-full flex flex-col bg-base-200 border-r border-base-300" style="padding-top: env(safe-area-inset-top);"> spawn_local(async move {
<div class="p-2"> let client = gloo_net::http::Request::post("/api/auth/logout");
<ul class="menu w-full rounded-box gap-1"> if let Ok(resp) = client.send().await {
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li> if resp.ok() {
<li> // Force full reload to clear state
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::All))} on:click=move |_| set_filter(crate::store::FilterStatus::All)> let _ = window().location().set_href("/login");
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> }
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> }
});
};
view! {
<div class="w-64 min-h-[100dvh] flex flex-col bg-base-200 border-r border-base-300" style="padding-top: env(safe-area-inset-top);">
<div class="p-2 flex-1 overflow-y-auto">
<ul class="menu w-full rounded-box gap-1">
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::All))} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
"All"
<span class="badge badge-sm badge-ghost ml-auto">{total_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Downloading))} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
"Downloading"
<span class="badge badge-sm badge-ghost ml-auto">{downloading_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Seeding))} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
"Seeding"
<span class="badge badge-sm badge-ghost ml-auto">{seeding_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Completed))} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"Completed"
<span class="badge badge-sm badge-ghost ml-auto">{completed_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Paused))} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
"Paused"
<span class="badge badge-sm badge-ghost ml-auto">{paused_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Inactive))} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
"Inactive"
<span class="badge badge-sm badge-ghost ml-auto">{inactive_count}</span>
</button>
</li>
</ul>
</div>
<div class="p-4 border-t border-base-300 bg-base-200/50">
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-8 rounded-full bg-neutral text-neutral-content ring ring-primary ring-offset-base-100 ring-offset-1">
<span class="text-sm font-bold flex items-center justify-center h-full">"A"</span>
</div>
</div>
<div class="flex-1 overflow-hidden">
<div class="font-bold text-sm truncate">"Admin User"</div>
<div class="text-[10px] text-base-content/60 truncate">"Online"</div>
</div>
<button
class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10"
title="Logout"
on:click=handle_logout
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg> </svg>
"All"
<span class="badge badge-sm badge-ghost ml-auto">{total_count}</span>
</button> </button>
</li> </div>
<li> </div>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Downloading))} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
"Downloading"
<span class="badge badge-sm badge-ghost ml-auto">{downloading_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Seeding))} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
"Seeding"
<span class="badge badge-sm badge-ghost ml-auto">{seeding_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Completed))} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"Completed"
<span class="badge badge-sm badge-ghost ml-auto">{completed_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Paused))} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
"Paused"
<span class="badge badge-sm badge-ghost ml-auto">{paused_count}</span>
</button>
</li>
<li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Inactive))} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
"Inactive"
<span class="badge badge-sm badge-ghost ml-auto">{inactive_count}</span>
</button>
</li>
</ul>
</div> </div>
}}
</div>
}
}