ppc login works in mme main
This commit is contained in:
389
parts/mme/src/dashboard.rs
Normal file
389
parts/mme/src/dashboard.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_components::{Button, ButtonSize, ButtonVariant, Control, ControlKind, Prompt};
|
||||
use dioxus_icons::lucide::{Bell, Bot, Check, Copy, LogIn, Mic, Moon, Power, Settings, Shield, Sun, Wifi};
|
||||
|
||||
use crate::{read_hub_state, sleep_ms, start_login};
|
||||
|
||||
fn copy_to_clipboard(text: &str) {
|
||||
let mut literal = String::from('"');
|
||||
for c in text.chars() {
|
||||
match c {
|
||||
'"' => literal.push_str("\\\""),
|
||||
'\\' => literal.push_str("\\\\"),
|
||||
'\n' => literal.push_str("\\n"),
|
||||
'\r' => literal.push_str("\\r"),
|
||||
'\t' => literal.push_str("\\t"),
|
||||
c if c.is_control() => literal.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c => literal.push(c),
|
||||
}
|
||||
}
|
||||
literal.push('"');
|
||||
let _ = document::eval(&format!("navigator.clipboard.writeText({literal})"));
|
||||
}
|
||||
|
||||
const APP_CSS: &str = r#"
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #171D18;
|
||||
color: #F2F4EA;
|
||||
font-family: "Segoe UI", "IBM Plex Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
#main, #main > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.mme-hub {
|
||||
/* Palette: dark defaults; light overrides below. Values match the mize token table. */
|
||||
--bg: #171D18;
|
||||
--surface: #242D25;
|
||||
--surface-2: #2C362D;
|
||||
--text: #F2F4EA;
|
||||
--text-2: #AAB5A4;
|
||||
--border: #3A463A;
|
||||
--accent: #D7E47D;
|
||||
--accent-text: #D7E47D;
|
||||
--success: #B9D987;
|
||||
--info: #B9E4EB;
|
||||
--warning: #FFD77F;
|
||||
--danger: #FF9B8E;
|
||||
--on-accent: #171D18;
|
||||
--focus-ring: var(--accent);
|
||||
--tile-size: 6.75rem;
|
||||
--tile-gap: 0.65rem;
|
||||
--prompt-gap: 2.25rem;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 1rem 1.25rem 1.5rem;
|
||||
gap: var(--prompt-gap);
|
||||
overflow: auto;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.mme-hub[data-theme="light"] {
|
||||
--bg: #F7F8F4;
|
||||
--surface: #FFFFFF;
|
||||
--surface-2: #EFF2EA;
|
||||
--text: #20251C;
|
||||
--text-2: #66705E;
|
||||
--border: #DFE4D9;
|
||||
--accent: #FEC251;
|
||||
--accent-text: #79500F;
|
||||
--success: #526F32;
|
||||
--info: #376777;
|
||||
--warning: #A66B13;
|
||||
--danger: #B94D42;
|
||||
--on-accent: #79500F;
|
||||
}
|
||||
.mme-topbar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
}
|
||||
.mme-topbar-prompt {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.mme-topbar-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.mme-login-banner {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.mme-login-banner-label {
|
||||
color: var(--text-2);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mme-login-banner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mme-login-banner-code {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--accent-text);
|
||||
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.mme-login-banner-url {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--info);
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.mme-tiles {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
justify-items: center;
|
||||
gap: var(--tile-gap);
|
||||
grid-template-columns: repeat(3, var(--tile-size));
|
||||
}
|
||||
.mme-hub-tile {
|
||||
width: var(--tile-size);
|
||||
height: var(--tile-size);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.mme-hub {
|
||||
--tile-size: 5.75rem;
|
||||
--prompt-gap: 1.35rem;
|
||||
}
|
||||
.mme-topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mme-topbar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mme-tiles {
|
||||
grid-template-columns: repeat(2, var(--tile-size));
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub fn launch() {
|
||||
dioxus::LaunchBuilder::new()
|
||||
.with_cfg(
|
||||
dioxus::desktop::Config::new()
|
||||
.with_menu(None)
|
||||
.with_window(
|
||||
dioxus::desktop::WindowBuilder::new().with_title("mme"),
|
||||
),
|
||||
)
|
||||
.launch(App);
|
||||
}
|
||||
|
||||
/// Isolated so hub re-renders never diff `document::Style` props.
|
||||
#[component]
|
||||
fn AppStyle() -> Element {
|
||||
rsx! {
|
||||
document::Style { {APP_CSS} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
rsx! {
|
||||
AppStyle {}
|
||||
document::Title { "mme" }
|
||||
Hub {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Hub() -> Element {
|
||||
let mut prompt = use_signal(String::new);
|
||||
let mut mic_on = use_signal(|| false);
|
||||
let mut wifi_on = use_signal(|| true);
|
||||
let mut shield_on = use_signal(|| true);
|
||||
let mut dark = use_signal(|| true);
|
||||
let mut bot_armed = use_signal(|| false);
|
||||
|
||||
let initial = read_hub_state().unwrap_or_default();
|
||||
let mut logged_in = use_signal(|| initial.logged_in);
|
||||
let mut logging_in = use_signal(|| initial.logging_in);
|
||||
let mut login_url = use_signal(|| initial.login_url.clone());
|
||||
let mut login_code = use_signal(|| initial.login_code.clone());
|
||||
|
||||
use_future(move || async move {
|
||||
loop {
|
||||
sleep_ms(200).await;
|
||||
let Ok(hub) = read_hub_state() else {
|
||||
continue;
|
||||
};
|
||||
// Only write signals when values change — avoids re-rendering the
|
||||
// whole hub (and its nested document::Style tags) every tick.
|
||||
if logged_in() != hub.logged_in {
|
||||
logged_in.set(hub.logged_in);
|
||||
}
|
||||
if logging_in() != hub.logging_in {
|
||||
logging_in.set(hub.logging_in);
|
||||
}
|
||||
if login_url() != hub.login_url {
|
||||
login_url.set(hub.login_url.clone());
|
||||
}
|
||||
if login_code() != hub.login_code {
|
||||
login_code.set(hub.login_code.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let theme = if dark() { "dark" } else { "light" };
|
||||
let show_banner = logging_in() && (login_url().is_some() || login_code().is_some());
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "mme-hub",
|
||||
"data-theme": theme,
|
||||
|
||||
div { class: "mme-topbar",
|
||||
div { class: "mme-topbar-prompt",
|
||||
Prompt {
|
||||
value: prompt(),
|
||||
placeholder: "Ask or run a command…".to_string(),
|
||||
oninput: move |e: FormEvent| prompt.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "mme-topbar-actions",
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| dark.set(!dark()),
|
||||
if dark() {
|
||||
Moon {}
|
||||
} else {
|
||||
Sun {}
|
||||
}
|
||||
if dark() { "Dark" } else { "Light" }
|
||||
}
|
||||
if logged_in() {
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| {},
|
||||
Check {}
|
||||
"Logged in"
|
||||
}
|
||||
} else if logging_in() {
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| {},
|
||||
LogIn {}
|
||||
"Waiting…"
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
variant: ButtonVariant::Primary,
|
||||
onclick: move |_| start_login(),
|
||||
LogIn {}
|
||||
"PPC Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show_banner {
|
||||
div { class: "mme-login-banner",
|
||||
span { class: "mme-login-banner-label", "PPC device login" }
|
||||
if let Some(code) = login_code() {
|
||||
div { class: "mme-login-banner-row",
|
||||
span { class: "mme-login-banner-code", "{code}" }
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
onclick: move |_| copy_to_clipboard(&code),
|
||||
Copy {}
|
||||
"Copy code"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(url) = login_url() {
|
||||
div { class: "mme-login-banner-row",
|
||||
span { class: "mme-login-banner-url", "{url}" }
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
onclick: move |_| copy_to_clipboard(&url),
|
||||
Copy {}
|
||||
"Copy link"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mme-tiles",
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Mic".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: mic_on(),
|
||||
confirm: Some("Enable microphone access for voice input?".into()),
|
||||
on_enabled_change: move |v| mic_on.set(v),
|
||||
Mic {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Alerts".to_string(),
|
||||
kind: ControlKind::Action,
|
||||
onclick: move |_| {},
|
||||
Bell {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Network".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: wifi_on(),
|
||||
on_enabled_change: move |v| wifi_on.set(v),
|
||||
Wifi {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Agent".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: bot_armed(),
|
||||
confirm: Some("Arm the agent for autonomous actions?".into()),
|
||||
on_enabled_change: move |v| bot_armed.set(v),
|
||||
Bot {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Status".to_string(),
|
||||
kind: ControlKind::Display,
|
||||
value: Some(if bot_armed() { "armed".into() } else { "idle".into() }),
|
||||
Power {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Shield".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: shield_on(),
|
||||
confirm: Some("Disable shield? This turns off local protections.".into()),
|
||||
on_enabled_change: move |v| shield_on.set(v),
|
||||
Shield {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Settings".to_string(),
|
||||
kind: ControlKind::Action,
|
||||
onclick: move |_| {},
|
||||
Settings {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user