Panel web: servers, capes, player profiles, auth server settings
- Servers: live status, players and TPS; add a server and get its token with copy-paste setup for the plugin/mod and authlib-injector; detail page with leaderboard (sortable), online players and activity feed - Capes: upload, preview, and choose who can wear each one - Players: click a player for their skin (upload, arm style, reset), cape, playtime and per-server stats; disable with a reason - Settings: Microsoft section replaced by the auth server (public address, Yggdrasil URL, start flag, signing key) - Dashboard: players online now and connected servers - Avatars come from the panel's own skins Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
19 files changed
+1284
-38
No files matched your search
@@ -35,10 +35,15 @@ pub async fn stats(_: AdminUser, State(state): State<AppState>) -> AppResult<Jso
|
||||
.await?;
|
||||
let recent: Vec<(Option<String>, Option<String>, String)> =
|
||||
sqlx::query_as("SELECT instance_id, username, created_at FROM events ORDER BY id DESC LIMIT 12").fetch_all(&state.db).await?;
|
||||
let players_7d: i64 = sqlx::query_scalar("SELECT COUNT(DISTINCT username) FROM events WHERE created_at >= ?")
|
||||
.bind((chrono::Utc::now() - chrono::Duration::days(7)).to_rfc3339())
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
// Anyone who launched the game or played on a connected server.
|
||||
let week = (chrono::Utc::now() - chrono::Duration::days(7)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let players_7d: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM (SELECT lower(username) FROM events WHERE username IS NOT NULL AND created_at >= ?1
|
||||
UNION SELECT lower(name) FROM player_stats WHERE last_seen >= ?1)",
|
||||
)
|
||||
.bind(&week)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
// Fill in empty days so the chart is continuous.
|
||||
let mut days = Vec::new();
|
||||
@@ -707,6 +712,29 @@ pub async fn admin_delete_skin(_: AdminUser, State(state): State<AppState>, Path
|
||||
Ok(Json(view(&state, user).await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AdminModelInput {
|
||||
model: String,
|
||||
}
|
||||
|
||||
pub async fn admin_set_model(
|
||||
_: AdminUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
Json(input): Json<AdminModelInput>,
|
||||
) -> AppResult<Json<AdminUserView>> {
|
||||
if !matches!(input.model.as_str(), "classic" | "slim") {
|
||||
return Err(AppError::bad_request("model must be classic or slim"));
|
||||
}
|
||||
sqlx::query("UPDATE users SET skin_model = ? WHERE id = ?").bind(&input.model).bind(id).execute(&state.db).await?;
|
||||
let user = sqlx::query_as("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("user not found"))?;
|
||||
Ok(Json(view(&state, user).await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AdminCapeInput {
|
||||
cape_id: Option<i64>,
|
||||
|
||||
Reference in new issue
Block a user