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

23
parts/mme/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-mme"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_mme"
crate-type = ["cdylib", "rlib"]
[dependencies]
log = "0.4.34"
mize = { workspace = true }
dioxus = { version = "0.7", features = ["desktop"] }
dioxus-components = { path = "../mize.ui/dioxus-components", features = ["desktop"] }
dioxus-icons = "0.1.0"
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

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 {}
}
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
use mize::prelude::*;
pub struct CliPart {
id: MizeId,
}
impl MizePart for CliPart {
fn id(&self) -> MizeResult<MizeId> {
Ok(self.id.clone())
}
}
impl CliPart {
pub fn get(name: &str) -> MizeResult<CliPart> {
let mut mize = get_global_mize()?;
let part = mize.get_part(name)?;
Ok(CliPart { id: part.id()? })
}
pub fn run(&self, cmdline: Vec<String>) -> MizeResult<()> {
let cmdline = cmdline.into_item_data();
let mut args = ItemData::new();
args.set_path("0", cmdline)?;
dyn_call(self.id.clone(), "run", args)?;
Ok(())
}
pub fn set_command(&self, name: &str) -> MizeResult<()> {
let name = name.into_item_data();
let mut args = ItemData::new();
args.set_path("0", name)?;
dyn_call(self.id.clone(), "set_command", args)?;
Ok(())
}
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
&self,
subcmd: Command,
action: T,
) -> MizeResult<()> {
let mut subcmd_arg = ItemData::new();
subcmd_arg.set_path("_native", subcmd.native_id)?;
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
action(matches)?;
Ok(ItemData::new())
}))?;
let mut args = ItemData::new();
args.set_path("0", subcmd_arg)?;
args.set_path("1", action_arg)?;
dyn_call(self.id.clone(), "subcommand", args)?;
Ok(())
}
}
pub struct Command {
native_id: u32,
}
pub struct ArgMatches {
native_id: u32,
}
impl FromItemData for ArgMatches {
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
Ok(ArgMatches {
native_id: data.get_path("_native")?.value_u32()?,
})
}
}
impl Command {
pub fn new(name: &str) -> MizeResult<Command> {
let name = name.into_item_data();
let mut args = ItemData::new();
args.set_path("0", name)?;
let mut mize = get_global_mize()?;
let id = mize.id_new_dots("cli.Command")?;
let data = dyn_call(id.clone(), "new", args)?;
let native_id: u32 = data.get_path("_native")?.native_take()?;
Ok(Command { native_id })
}
}

97
parts/mme/src/lib.rs Normal file
View File

@@ -0,0 +1,97 @@
mod dashboard;
mod generated;
use generated::{CliPart, Command};
use mize::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HubState {
pub login_url: Option<String>,
pub login_code: Option<String>,
pub logged_in: bool,
pub logging_in: bool,
}
#[unsafe(no_mangle)]
extern "C" fn part_mme() -> MizeResult<()> {
opt("hub").fun(Box::new(|| Ok(ItemData::from_native(HubState::default())?)));
method("set_url_and_code", "String, String -> ()", |args| {
let url = args.get_path("0")?.value_string()?;
let code = args.get_path("1")?.value_string()?;
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
hub.login_url = Some(url);
hub.login_code = Some(code);
hub.logged_in = false;
hub.logging_in = true;
Ok(ItemData::new())
});
method("set_login_state", "bool -> ()", |args| {
let logged_in = args.get_path("0")?.value_bool()?;
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
hub.logged_in = logged_in;
hub.logging_in = false;
if logged_in {
hub.login_url = None;
hub.login_code = None;
}
Ok(ItemData::new())
});
// Seed logged-in from existing secrets, if any.
if let Ok(secrets) = get_config("auth.client.secrets") {
if !secrets.cbor().is_null() {
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
hub.logged_in = true;
}
}
let cli = CliPart::get("cli")?;
cli.set_command("mme")?;
cli.subcommand(Command::new("main")?, |_| {
dashboard::launch();
Ok(())
})?;
Ok(())
}
pub fn read_hub_state() -> MizeResult<HubState> {
Ok(get_config("mme.hub")?.native::<HubState>()?.clone())
}
pub fn start_login() {
std::thread::spawn(|| {
let result = (|| -> MizeResult<()> {
{
let mut hub = get_config("mme.hub")?.native_mut::<HubState>()?;
hub.logging_in = true;
hub.logged_in = false;
}
let mut mize = get_global_mize()?;
dyn_call(mize.id_new_dots("auth.client")?, "login", ItemData::new())?;
Ok(())
})();
if let Err(err) = result {
out!("mme ppc login failed: {:?}", err);
let _ = (|| -> MizeResult<()> {
let mut hub = get_config("mme.hub")?.native_mut::<HubState>()?;
hub.logging_in = false;
hub.logged_in = false;
Ok(())
})();
}
});
}
/// Tiny async sleep without pulling tokio into the UI crate.
pub async fn sleep_ms(ms: u64) {
let (tx, rx) = mize::flume::bounded(0);
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(ms));
let _ = tx.send(());
});
let _ = rx.recv_async().await;
}