From 5509a6d22274cd48e51f540595f4c9a3d0a43a4b Mon Sep 17 00:00:00 2001
From: Mitchell M
Date: Tue, 6 Oct 2026 11:36:34 -0500
Subject: [PATCH] add manage page
---
src/app.rs | 19 ++----
src/lib.rs | 1 +
src/manage.rs | 123 ++++++++++++++++++++++++++++++++++++
style/main.scss | 162 +++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 288 insertions(+), 17 deletions(-)
create mode 100644 src/manage.rs
diff --git a/src/app.rs b/src/app.rs
index f5d6400..993bb9e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,6 +5,8 @@ use leptos_router::{
StaticSegment,
};
+use crate::manage::ManagePage;
+
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -34,28 +36,15 @@ pub fn App() -> impl IntoView {
// sets the document title
-
+
// content for this welcome page
-
+
}
}
-
-/// Renders the home page of your application.
-#[component]
-fn HomePage() -> impl IntoView {
- // Creates a reactive value to update the button
- let count = RwSignal::new(0);
- let on_click = move |_| *count.write() += 1;
-
- view! {
- "Welcome to Leptos!"
-
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
index 151489d..c52c809 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
pub mod app;
+pub mod manage;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
diff --git a/src/manage.rs b/src/manage.rs
new file mode 100644
index 0000000..642e8d6
--- /dev/null
+++ b/src/manage.rs
@@ -0,0 +1,123 @@
+use leptos::prelude::*;
+
+#[cfg(feature = "ssr")]
+use std::sync::atomic::{AtomicBool, Ordering};
+
+#[cfg(feature = "ssr")]
+static SERVER_LIVE: AtomicBool = AtomicBool::new(false);
+
+#[cfg(feature = "ssr")]
+const STATUS_SECRET_KEY: &str = "password";
+
+#[server]
+async fn get_live_state() -> Result {
+ Ok(SERVER_LIVE.load(Ordering::SeqCst))
+}
+
+#[server]
+async fn set_live_state(live: bool, secret_key: String) -> Result<(), ServerFnError> {
+ if secret_key != STATUS_SECRET_KEY {
+ return Err(ServerFnError::ServerError("Invalid secret key".to_string()));
+ }
+
+ SERVER_LIVE.store(live, Ordering::SeqCst);
+ Ok(())
+}
+
+/// Renders the server management page.
+#[component]
+pub fn ManagePage() -> impl IntoView {
+ let live_state = Resource::new(|| (), |_| get_live_state());
+
+ view! {
+
+ "CONTROL PANEL"
+ "Server status"
+ "Manage whether your server is accepting live traffic."
+ "Loading server status..."
}>
+ {move || Suspend::new(async move {
+ match live_state.await {
+ Ok(is_live) => view! { }.into_any(),
+ Err(error) => view! {
+
+ "Unable to load server status: " {error.to_string()}
+
+ }.into_any(),
+ }
+ })}
+
+
+ }
+}
+
+#[component]
+fn LiveToggle(initial: bool) -> impl IntoView {
+ let (live, set_live) = signal(initial);
+ let (server_live, set_server_live) = signal(initial);
+ let (updating, set_updating) = signal(false);
+ let (secret_key, set_secret_key) = signal(String::new());
+ let (error, set_error) = signal(None::);
+
+ let on_click = move |_| {
+ let previous = live.get_untracked();
+ let checked = !previous;
+ let key = secret_key.get_untracked();
+ set_live.set(checked);
+ set_updating.set(true);
+ set_error.set(None);
+
+ leptos::task::spawn_local(async move {
+ match set_live_state(checked, key).await {
+ Ok(()) => set_server_live.set(checked),
+ Err(error) => {
+ set_live.set(previous);
+ set_error.set(Some(error.to_string()));
+ }
+ }
+ set_updating.set(false);
+ });
+ };
+
+ view! {
+
+
+
+
+
+
+ "Server status: "
+ {move || if server_live.get() { "Live" } else { "Offline" }}
+
+ {move || error.get().map(|message| view! {
+
{message}
+ })}
+
+ }
+}
diff --git a/style/main.scss b/style/main.scss
index e4538e1..d659011 100644
--- a/style/main.scss
+++ b/style/main.scss
@@ -1,4 +1,162 @@
body {
- font-family: sans-serif;
- text-align: center;
+ min-height: 100vh;
+ margin: 0;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ box-sizing: border-box;
+ background:
+ radial-gradient(ellipse at 15% 10%, rgba(31, 111, 235, 0.2), transparent 38%),
+ #0b1220;
+ color: #e6edf7;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+main {
+ width: min(100%, 480px);
+}
+
+.status-card {
+ padding: clamp(28px, 7vw, 48px);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 24px;
+ background: rgba(17, 27, 45, 0.92);
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
+ text-align: left;
+}
+
+.eyebrow {
+ margin: 0 0 12px;
+ color: #7cb3ff;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.16em;
+}
+
+h1 {
+ margin: 0;
+ font-size: clamp(2rem, 8vw, 2.6rem);
+ letter-spacing: -0.04em;
+}
+
+.intro {
+ margin: 12px 0 32px;
+ color: #a9b6c9;
+ line-height: 1.6;
+}
+
+.status-controls {
+ display: grid;
+ gap: 12px;
+}
+
+.key-label {
+ color: #d3dceb;
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.key-input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 13px 15px;
+ border: 1px solid #35445c;
+ border-radius: 10px;
+ outline: none;
+ background: #0c1524;
+ color: #f5f8fc;
+ font: inherit;
+ transition: border-color 150ms ease, box-shadow 150ms ease;
+}
+
+.key-input::placeholder {
+ color: #74839a;
+}
+
+.key-input:focus {
+ border-color: #65a4ff;
+ box-shadow: 0 0 0 3px rgba(101, 164, 255, 0.18);
+}
+
+.status-button {
+ margin-top: 8px;
+ padding: 14px 18px;
+ border: 0;
+ border-radius: 10px;
+ color: white;
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+ transition: background 150ms ease, transform 150ms ease, opacity 150ms ease;
+}
+
+.status-button.go-live {
+ background: #16804a;
+}
+
+.status-button.go-live:hover:not(:disabled) {
+ background: #1b9959;
+}
+
+.status-button.take-offline {
+ background: #b54739;
+}
+
+.status-button.take-offline:hover:not(:disabled) {
+ background: #d05242;
+}
+
+.status-button:hover:not(:disabled) {
+ transform: translateY(-1px);
+}
+
+.status-button:disabled {
+ opacity: 0.65;
+ cursor: wait;
+}
+
+.status-indicator {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ margin: 14px 0 0;
+ color: #a9b6c9;
+ font-size: 0.95rem;
+}
+
+.status-indicator strong {
+ color: #d3dceb;
+}
+
+.status-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background: #8491a5;
+}
+
+.status-indicator.is-live .status-dot {
+ background: #37d488;
+ box-shadow: 0 0 12px rgba(55, 212, 136, 0.65);
+}
+
+.status-indicator.is-live strong {
+ color: #6ee7a8;
+}
+
+.loading {
+ color: #a9b6c9;
+}
+
+.error-message {
+ margin: 8px 0 0;
+ color: #ff9387;
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition-duration: 0.01ms !important;
+ }
}
\ No newline at end of file