Fix achievement icons in launcher, allow opening OAuth URLs, live map docs

- Shared achievement icon catalogue; launcher resolves icon/background/border ids
- Tauri opener capability now allows http(s) URLs (fixes Discord sign-in)
- Rewrite live map docs, admin guide progression section

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
Claude committed 2026-09-30 17:31:24 +00:00
1 parent 3ceed4382f
commit 1a321336b4
17 files changed
+511 -151

No files matched your search

+34 -6
View File
@@ -60,15 +60,29 @@ pub async fn game_config_route(GameServer(server): GameServer) -> Json<Value> {
}
/// Timestamp tables of the regions the panel already has, base64 per region.
pub async fn game_manifest(GameServer(server): GameServer, State(state): State<AppState>, Query(q): Query<DimQuery>) -> AppResult<Json<Value>> {
pub async fn game_manifest(
GameServer(server): GameServer,
State(state): State<AppState>,
Query(q): Query<DimQuery>,
) -> AppResult<Json<Value>> {
require_enabled(server.live_map_enabled)?;
let dim = dim_of(&q.dim)?;
let b64 = base64::engine::general_purpose::STANDARD;
let regions: Vec<Value> = state.livemap.manifest(server.id, dim).into_iter().map(|(x, z, ts)| json!({ "x": x, "z": z, "timestamps": b64.encode(ts) })).collect();
let regions: Vec<Value> = state
.livemap
.manifest(server.id, dim)
.into_iter()
.map(|(x, z, ts)| json!({ "x": x, "z": z, "timestamps": b64.encode(ts) }))
.collect();
Ok(Json(json!({ "dimension": dim.slug(), "regions": regions })))
}
pub async fn game_chunks(GameServer(server): GameServer, State(state): State<AppState>, Query(q): Query<DimQuery>, body: Bytes) -> AppResult<Json<Value>> {
pub async fn game_chunks(
GameServer(server): GameServer,
State(state): State<AppState>,
Query(q): Query<DimQuery>,
body: Bytes,
) -> AppResult<Json<Value>> {
require_enabled(server.live_map_enabled)?;
let dim = dim_of(&q.dim)?;
let chunks = parse_chunks(&body).map_err(AppError::bad_request)?;
@@ -102,7 +116,11 @@ fn sanitize(mut players: Vec<LivePlayer>) -> Vec<LivePlayer> {
players
}
pub async fn game_players(GameServer(server): GameServer, State(state): State<AppState>, Json(b): Json<PlayersBody>) -> AppResult<Json<Value>> {
pub async fn game_players(
GameServer(server): GameServer,
State(state): State<AppState>,
Json(b): Json<PlayersBody>,
) -> AppResult<Json<Value>> {
require_enabled(server.live_map_enabled)?;
state.livemap.set_players(server.id, sanitize(b.players));
Ok(Json(json!({ "ok": true })))
@@ -140,7 +158,12 @@ async fn ws_loop(mut socket: WebSocket, state: AppState, id: i64) {
}
Message::Binary(b) => {
let reply = match b.split_first() {
Some((d, rest)) => match Dim::ALL.get(*d as usize).copied().ok_or("unknown dimension").and_then(|dim| parse_chunks(rest).map(|c| (dim, c))) {
Some((d, rest)) => match Dim::ALL
.get(*d as usize)
.copied()
.ok_or("unknown dimension")
.and_then(|dim| parse_chunks(rest).map(|c| (dim, c)))
{
Ok((dim, chunks)) => match state.livemap.ingest_chunks(id, dim, chunks).await {
Ok(n) => json!({ "type": "stored", "count": n }),
Err(e) => json!({ "type": "error", "error": e.to_string() }),
@@ -236,7 +259,12 @@ pub async fn admin_reset(_: AdminUser, State(state): State<AppState>, Path(id):
/// Authenticated reverse proxy to the per-server Vantage sidecar. Clients
/// use `…/api/livemap/{server}/{dimension}/v1/worlds/default/manifest.json`
/// and everything the manifest references.
pub async fn proxy(_: AuthUser, State(state): State<AppState>, Path((id, dim, path)): Path<(i64, String, String)>, headers: HeaderMap) -> AppResult<Response> {
pub async fn proxy(
_: AuthUser,
State(state): State<AppState>,
Path((id, dim, path)): Path<(i64, String, String)>,
headers: HeaderMap,
) -> AppResult<Response> {
let server = get_server(&state, id).await?;
require_enabled(server.live_map_enabled)?;
let dim = dim_of(&dim)?;
+24 -10
View File
@@ -11,11 +11,10 @@ use serde::Deserialize;
use serde_json::{json, Value};
async fn pool_counts(state: &AppState) -> AppResult<Value> {
let rows: Vec<(String, i64, i64)> = sqlx::query_as(
"SELECT period, COUNT(*), COALESCE(SUM(pinned), 0) FROM quests WHERE enabled = 1 GROUP BY period",
)
.fetch_all(&state.db)
.await?;
let rows: Vec<(String, i64, i64)> =
sqlx::query_as("SELECT period, COUNT(*), COALESCE(SUM(pinned), 0) FROM quests WHERE enabled = 1 GROUP BY period")
.fetch_all(&state.db)
.await?;
let mut out = json!({ "daily": { "enabled": 0, "pinned": 0 }, "weekly": { "enabled": 0, "pinned": 0 } });
for (period, n, pinned) in rows {
if period == "daily" || period == "weekly" {
@@ -45,9 +44,11 @@ pub async fn put_settings(admin: AdminUser, State(state): State<AppState>, Json(
let before = progression::load_pool(&state.db).await?;
progression::save(&state.db, &next).await?;
progression::set_curve(&next);
let curve_changed = before.level_base != next.level_base || before.level_exponent != next.level_exponent || before.max_level != next.max_level;
let curve_changed =
before.level_base != next.level_base || before.level_exponent != next.level_exponent || before.max_level != next.max_level;
let recalculated = if curve_changed { progression::recalculate_levels(&state.db).await? } else { 0 };
crate::routes::activity::record(&state, &admin, "panel", "progression_settings", Some(&format!("recalculated {recalculated} levels"))).await?;
crate::routes::activity::record(&state, &admin, "panel", "progression_settings", Some(&format!("recalculated {recalculated} levels")))
.await?;
let mut v = view(&state, &next).await?;
v["recalculated"] = json!(recalculated);
Ok(Json(v))
@@ -139,7 +140,12 @@ pub struct Adjust {
reason: String,
}
pub async fn adjust_player(admin: AdminUser, State(state): State<AppState>, Path(uuid): Path<String>, Json(a): Json<Adjust>) -> AppResult<Json<Value>> {
pub async fn adjust_player(
admin: AdminUser,
State(state): State<AppState>,
Path(uuid): Path<String>,
Json(a): Json<Adjust>,
) -> AppResult<Json<Value>> {
let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE uuid = ?)").bind(&uuid).fetch_one(&state.db).await?;
if !exists {
return Err(AppError::not_found("player not found"));
@@ -150,7 +156,8 @@ pub async fn adjust_player(admin: AdminUser, State(state): State<AppState>, Path
"global" => ("global".to_string(), progression::adjust_global(&mut tx, &uuid, a.mode, a.amount, &now).await?),
"server" => {
let id = a.server_id.ok_or_else(|| AppError::bad_request("pick a server"))?;
let name: Option<String> = sqlx::query_scalar("SELECT name FROM game_servers WHERE id = ?").bind(id).fetch_optional(&mut *tx).await?;
let name: Option<String> =
sqlx::query_scalar("SELECT name FROM game_servers WHERE id = ?").bind(id).fetch_optional(&mut *tx).await?;
let name = name.ok_or_else(|| AppError::not_found("server not found"))?;
(format!("server {name}"), progression::adjust_server(&mut tx, id, &uuid, a.mode, a.amount, &now).await?)
}
@@ -163,7 +170,14 @@ pub async fn adjust_player(admin: AdminUser, State(state): State<AppState>, Path
tx.commit().await?;
let reason: String = a.reason.chars().filter(|c| !c.is_control()).take(120).collect();
let detail = format!("{label} XP {} → {} (level {} → {}) for {uuid}{}", change.old_xp, change.new_xp, change.old_level, change.new_level, if reason.is_empty() { String::new() } else { format!(": {reason}") });
let detail = format!(
"{label} XP {} → {} (level {} → {}) for {uuid}{}",
change.old_xp,
change.new_xp,
change.old_level,
change.new_level,
if reason.is_empty() { String::new() } else { format!(": {reason}") }
);
crate::routes::activity::record(&state, &admin, "panel", "xp_adjust", Some(&detail)).await?;
Ok(Json(player_detail(&state, &uuid).await?))
}