crates/gunnar-ui-web/src/pages/account_totals.rs
//! **Administration → Accounts** (owner's enterprise list, 2026-10-05): the
//! operator's view of the accounts on this box — TOTALS ONLY. How many
//! accounts, members and repositories, storage used and sold, health; never a
//! repository name, a member name or an event of an account. That is gunnar's
//! rule (a server-scope grant stops at an account's door), not this page's:
//! the page draws what `Enterprise.Aggregates` answers.
//!
//! The one way inside an account is asking its admin: a reason and a length,
//! sent to gunnar (`/api/accounts/looks`), which answers a code to tell the
//! account admin. The admin approves it on their own Audit log page.
use leptos::prelude::*;
use crate::enterprise::{look_line, totals_rows, Look, Looks, Totals};
use crate::ui::{FailBox, Hint, Loading, PageHeader, Panel};
#[component]
pub fn AccountTotals() -> impl IntoView {
let totals = LocalResource::new(|| async { crate::api::get::<Totals>("/api/accounts/aggregates").await });
let looks = LocalResource::new(|| async { crate::api::get::<Looks>("/api/accounts/looks").await });
let account = RwSignal::new(String::new());
let reason = RwSignal::new(String::new());
let minutes = RwSignal::new("60".to_owned());
let asked = RwSignal::new(None::<Look>);
let failed = RwSignal::new(None::<crate::api::Fail>);
let ask = move |_| {
let body = serde_json::json!({
"account": account.get_untracked().trim(),
"reason": reason.get_untracked().trim(),
"minutes": minutes.get_untracked().trim().parse::<u32>().unwrap_or(60),
});
leptos::task::spawn_local(async move {
match crate::api::post::<_, Look>("/api/accounts/looks", &body).await {
Ok(look) => {
failed.set(None);
asked.set(Some(look));
looks.refetch();
totals.refetch();
}
Err(fail) => failed.set(Some(fail)),
}
});
};
let end = move |id: String| {
let path = format!("/api/accounts/looks/{}/end", crate::api::segment(&id));
leptos::task::spawn_local(async move {
match crate::api::post_empty::<serde_json::Value>(&path).await {
Ok(_) => {
failed.set(None);
looks.refetch();
}
Err(fail) => failed.set(Some(fail)),
}
});
};
view! {
<PageHeader title="Accounts"/>
<div class="stack" id="accounts-totals">
<Panel title="Totals">
<Hint line="Totals only: no account's repositories, members or events."
more="Gunnar shows the operator what is inside an account only while its admin has approved a look. What still reaches the operator is stated in Gunnar's docs: the names in clone URLs, timing, and these totals."/>
{move || match totals.get() {
None => view! { <Loading/> }.into_any(),
Some(Err(fail)) => view! { <FailBox fail=fail/> }.into_any(),
Some(Ok(t)) => view! {
<dl class="kv" id="totals">
{totals_rows(&t).into_iter().map(|(label, value)| view! {
<dt>{label}</dt><dd data-total=label>{value}</dd>
}).collect_view()}
</dl>
}.into_any(),
}}
</Panel>
<Panel title="Ask to look inside an account">
<div class="form-row">
<label class="field">"Account"
<input id="look-account" placeholder="acme" prop:value=move || account.get()
on:input=move |ev| account.set(event_target_value(&ev))/>
</label>
<label class="field">"Why"
<input id="look-reason" placeholder="ticket 4711" prop:value=move || reason.get()
on:input=move |ev| reason.set(event_target_value(&ev))/>
</label>
<label class="field">"Minutes"
<input id="look-minutes" inputmode="numeric" prop:value=move || minutes.get()
on:input=move |ev| minutes.set(event_target_value(&ev))/>
</label>
<button class="btn primary" id="look-ask" on:click=ask>"Ask"</button>
</div>
{move || asked.get().map(|l| view! {
<p class="hint" id="look-asked" data-code=l.code.clone()>{format!("Asked. Give {} the code {}.", l.account, l.code)}</p>
})}
{move || match looks.get() {
None => view! { <Loading/> }.into_any(),
Some(Err(fail)) => view! { <FailBox fail=fail/> }.into_any(),
Some(Ok(l)) => view! {
<ul id="my-looks">
{l.requests.iter().map(|r| {
let id = r.id.clone();
let live = r.state == "approved" || r.state == "pending";
view! {
<li data-look=r.id.clone() data-state=r.state.clone()>
<span>{format!("{}: {}", r.account, look_line(r))}</span>" "
{live.then(|| view! {
<button class="btn" on:click=move |_| end(id.clone())>"End"</button>
})}
</li>
}
}).collect_view()}
</ul>
}.into_any(),
}}
{move || failed.get().map(|fail| view! { <FailBox fail=fail/> })}
</Panel>
</div>
}
}