ppc login works in mme main

This commit is contained in:
Sebastian Moser
2026-11-01 14:49:38 +01:00
parent 66b8713e3a
commit 6eedb1826a
68 changed files with 4123 additions and 1467 deletions

389
parts/mme/src/dashboard.rs Normal file
View 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 {}
}
}
}
}
}
}