Add launcher engine (scopenet-core) and admin panel server

- scopenet-shared: wire types for branding, auth, instances, manifests
- scopenet-core: Mojang versions/libraries/assets, automatic Java runtimes,
  Fabric/Quilt/Forge/NeoForge installation, file sync, launch command
  builder, Microsoft auth, servers.dat injection, server list ping
- scopenet-panel: Axum + SQLite admin API with JWT auth, users/groups,
  branding, settings, instances, Modrinth/CurseForge/zip import, file
  hosting and launch stats

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
Claude committed 2026-09-28 04:22:17 +00:00
1 parent 915f40a4c4
commit 8cdf616a94
43 files changed
+9317

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
use scopenet_panel::{app, bootstrap_admin, build_state, config::Config, db};
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// `scopenet-panel healthcheck` — used by the Docker HEALTHCHECK so the
// image doesn't need curl.
if std::env::args().nth(1).as_deref() == Some("healthcheck") {
return healthcheck().await;
}
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,tower_http=warn,sqlx=warn")))
.compact()
.init();
let cfg = Config::from_env();
let pool = db::connect(&cfg.data_dir).await?;
if !cfg.web_dir.join("index.html").exists() {
tracing::warn!("web UI not found at {} (build panel/web or set SCOPENET_WEB_DIR)", cfg.web_dir.display());
}
let bind = cfg.bind.clone();
let state = build_state(cfg, pool).await?;
bootstrap_admin(&state).await?;
let listener = tokio::net::TcpListener::bind(&bind).await?;
tracing::info!("ScopeNet panel v{} listening on http://{bind}", env!("CARGO_PKG_VERSION"));
axum::serve(listener, app(state)).with_graceful_shutdown(shutdown()).await?;
Ok(())
}
async fn shutdown() {
let ctrl_c = async { tokio::signal::ctrl_c().await.ok(); };
#[cfg(unix)]
let term = async {
if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
s.recv().await;
}
};
#[cfg(not(unix))]
let term = std::future::pending::<()>();
tokio::select! { _ = ctrl_c => {}, _ = term => {} }
tracing::info!("shutting down");
}
async fn healthcheck() -> anyhow::Result<()> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let bind = std::env::var("SCOPENET_BIND").unwrap_or_else(|_| "0.0.0.0:8080".into());
let port = bind.rsplit(':').next().unwrap_or("8080");
let mut s = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")).await?;
s.write_all(b"GET /healthz HTTP/1.0\r\nHost: localhost\r\n\r\n").await?;
let mut buf = String::new();
s.read_to_string(&mut buf).await?;
if buf.starts_with("HTTP/1.1 200") || buf.starts_with("HTTP/1.0 200") {
Ok(())
} else {
anyhow::bail!("unhealthy: {}", buf.lines().next().unwrap_or(""))
}
}