anvilsign in

collin/anvil

1//! Admin-only dashboard pages (site-level), gated by `User.is_admin`.
2
3use anvil_core::{
4 App,
5 usage,
6};
7use axum::{
8 Router,
9 extract::State,
10 response::{
11 IntoResponse,
12 Response,
13 },
14 routing::get,
15};
16use maud::{
17 Markup,
18 html,
19};
20
21use crate::{
22 auth::CurrentUser,
23 ui::{
24 fmt_size,
25 layout,
26 not_found,
27 server_error,
28 },
29};
30
31pub fn routes(router: Router<App>) -> Router<App> {
32 router.route("/-/admin/usage", get(usage_page))
33}
34
35/// `GET /-/admin/usage` — site-wide disk usage by user and content type.
36/// Non-admins (including anonymous) get a 404, so the page's existence isn't
37/// leaked. Tries to use cached value first; computes on-demand if not found.
38async fn usage_page(State(app): State<App>, CurrentUser(user): CurrentUser) -> Response {
39 if !user.as_ref().is_some_and(|u| u.is_admin) {
40 return not_found("not found");
41 }
42
43 // Try to get cached disk usage first
44 let data = if let Ok(Some(cached)) = anvil_core::admin_cache::get(&app.db, "disk_usage").await {
45 match serde_json::from_str(&cached.value) {
46 Ok(d) => d,
47 Err(_) => {
48 // Cache corrupted, recompute
49 match usage::compute(&app).await {
50 Ok(d) => d,
51 Err(e) => return server_error(e),
52 }
53 }
54 }
55 } else {
56 // No cached value, compute on demand
57 match usage::compute(&app).await {
58 Ok(d) => d,
59 Err(e) => return server_error(e),
60 }
61 };
62
63 layout("Disk usage", user.as_ref(), render(&data)).into_response()
64}
65
66fn render(u: &usage::Usage) -> Markup {
67 html! {
68 h1 { "Disk usage" }
69 p.muted {
70 "Actual on-disk bytes per user, by content type — "
71 (fmt_size(u.total() as i64)) " total across the instance."
72 }
73 table.usage {
74 thead { tr {
75 th { "User" }
76 th.num { "Repositories" }
77 th.num { "CI artifacts" }
78 th.num { "Attachments" }
79 th.num { "Total" }
80 } }
81 tbody {
82 @for row in &u.per_user {
83 tr {
84 td { (row.username) }
85 td.num { (fmt_size(row.git as i64)) }
86 td.num { (fmt_size(row.artifacts as i64)) }
87 td.num { (fmt_size(row.attachments as i64)) }
88 td.num { (fmt_size(row.total() as i64)) }
89 }
90 }
91 }
92 tfoot { tr {
93 td { "All users" }
94 td.num { (fmt_size(u.git as i64)) }
95 td.num { (fmt_size(u.artifacts as i64)) }
96 td.num { (fmt_size(u.attachments as i64)) }
97 td.num { (fmt_size(u.total() as i64)) }
98 } }
99 }
100 }
101}