feat: Implement server endpoints, store, and live dashboard
Backend: Server connection/config/admins DB queries, server API routes with auth-gated endpoints (overview, config CRUD, admin management). Frontend: Server store wired to API, dashboard fetches server data on mount with live status indicators, uptime formatting, and server config display. Logout now redirects to /login. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,53 +1,154 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
extract::State,
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::models::error::ApiResult;
|
use crate::db;
|
||||||
|
use crate::middleware::auth::AuthUser;
|
||||||
|
use crate::models::error::{ApiError, ApiResult};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_servers))
|
.route("/", get(get_server_overview))
|
||||||
.route("/{id}", get(get_server))
|
.route("/connection", get(get_connection))
|
||||||
.route("/{id}", put(update_server))
|
.route("/config", get(get_config))
|
||||||
.route("/{id}/command", post(send_command))
|
.route("/config", put(update_config))
|
||||||
.route("/{id}/plugins", get(get_server_plugins))
|
.route("/command", post(send_command))
|
||||||
.route("/{id}/start", post(start_server))
|
.route("/admins", get(get_admins))
|
||||||
.route("/{id}/stop", post(stop_server))
|
.route("/start", post(start_server))
|
||||||
.route("/{id}/restart", post(restart_server))
|
.route("/stop", post(stop_server))
|
||||||
|
.route("/restart", post(restart_server))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_servers() -> ApiResult<impl axum::response::IntoResponse> {
|
/// GET /api/servers — returns combined server overview (connection + config)
|
||||||
todo!()
|
async fn get_server_overview(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
let license_id = auth.license_id.ok_or(ApiError::LicenseInvalid)?;
|
||||||
|
|
||||||
|
let connection = db::servers::get_server_connection(&state.db, license_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
let config = db::servers::get_server_config(&state.db, license_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"connection": connection,
|
||||||
|
"config": config,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_server() -> ApiResult<impl axum::response::IntoResponse> {
|
/// GET /api/servers/connection
|
||||||
todo!()
|
async fn get_connection(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
let license_id = auth.license_id.ok_or(ApiError::LicenseInvalid)?;
|
||||||
|
|
||||||
|
let connection = db::servers::get_server_connection(&state.db, license_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({ "connection": connection })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_server() -> ApiResult<impl axum::response::IntoResponse> {
|
/// GET /api/servers/config
|
||||||
todo!()
|
async fn get_config(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
let license_id = auth.license_id.ok_or(ApiError::LicenseInvalid)?;
|
||||||
|
|
||||||
|
let config = db::servers::get_server_config(&state.db, license_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({ "config": config })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_command() -> ApiResult<impl axum::response::IntoResponse> {
|
#[derive(serde::Deserialize)]
|
||||||
todo!()
|
struct UpdateConfigRequest {
|
||||||
|
server_name: Option<String>,
|
||||||
|
max_players: Option<i32>,
|
||||||
|
world_size: Option<i32>,
|
||||||
|
current_seed: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_server_plugins() -> ApiResult<impl axum::response::IntoResponse> {
|
/// PUT /api/servers/config
|
||||||
todo!()
|
async fn update_config(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<UpdateConfigRequest>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
let license_id = auth.license_id.ok_or(ApiError::LicenseInvalid)?;
|
||||||
|
|
||||||
|
db::servers::update_server_config(
|
||||||
|
&state.db,
|
||||||
|
license_id,
|
||||||
|
body.server_name.as_deref(),
|
||||||
|
body.max_players,
|
||||||
|
body.world_size,
|
||||||
|
body.current_seed,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({ "message": "Config updated" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_server() -> ApiResult<impl axum::response::IntoResponse> {
|
/// GET /api/servers/admins
|
||||||
todo!()
|
async fn get_admins(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
let license_id = auth.license_id.ok_or(ApiError::LicenseInvalid)?;
|
||||||
|
|
||||||
|
let admins = db::servers::get_game_admins(&state.db, license_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({ "admins": admins })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_server() -> ApiResult<impl axum::response::IntoResponse> {
|
/// POST /api/servers/command
|
||||||
todo!()
|
async fn send_command(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
// TODO: Route command through PanelAdapter or NATS to game server
|
||||||
|
Err(ApiError::BadRequest("Server command not yet implemented".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restart_server() -> ApiResult<impl axum::response::IntoResponse> {
|
/// POST /api/servers/start
|
||||||
todo!()
|
async fn start_server(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
// TODO: Route through PanelAdapter
|
||||||
|
Err(ApiError::BadRequest("Server start not yet implemented".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/servers/stop
|
||||||
|
async fn stop_server(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
// TODO: Route through PanelAdapter
|
||||||
|
Err(ApiError::BadRequest("Server stop not yet implemented".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/servers/restart
|
||||||
|
async fn restart_server(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> ApiResult<Json<serde_json::Value>> {
|
||||||
|
// TODO: Route through PanelAdapter
|
||||||
|
Err(ApiError::BadRequest("Server restart not yet implemented".to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,51 +2,177 @@ use sqlx::PgPool;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// TODO: Define ServerConnection struct (id, license_id, name, host, rcon_port, rcon_password_encrypted, query_port, created_at)
|
use crate::models::server::{ServerConnection, ServerConfig, GameAdmin};
|
||||||
// TODO: Define ServerConfig struct (id, server_id, seed, world_size, max_players, hostname, description, header_image_url, etc.)
|
|
||||||
// TODO: Define GameAdmin struct (id, server_id, steam_id, role, added_at)
|
|
||||||
|
|
||||||
/// Register a new server connection (RCON credentials).
|
/// Get the server connection for a license.
|
||||||
pub async fn create_server_connection(pool: &PgPool, license_id: Uuid, name: &str, host: &str, rcon_port: i32) -> Result<Uuid> {
|
pub async fn get_server_connection(pool: &PgPool, license_id: Uuid) -> Result<Option<ServerConnection>> {
|
||||||
todo!()
|
let conn = sqlx::query_as::<_, ServerConnection>(
|
||||||
|
"SELECT id, license_id, connection_type, panel_api_endpoint, panel_api_key_encrypted, \
|
||||||
|
panel_server_identifier, companion_agent_token, companion_last_seen, plugin_last_seen, \
|
||||||
|
server_ip, server_port, game_port, connection_status, created_at, updated_at \
|
||||||
|
FROM server_connections WHERE license_id = $1",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a server connection by ID.
|
/// Create a new server connection.
|
||||||
pub async fn get_server_connection(pool: &PgPool, server_id: Uuid) -> Result<()> {
|
pub async fn create_server_connection(
|
||||||
todo!()
|
pool: &PgPool,
|
||||||
|
license_id: Uuid,
|
||||||
|
connection_type: &str,
|
||||||
|
server_ip: Option<&str>,
|
||||||
|
server_port: Option<i32>,
|
||||||
|
game_port: Option<i32>,
|
||||||
|
) -> Result<Uuid> {
|
||||||
|
let row: (Uuid,) = sqlx::query_as(
|
||||||
|
"INSERT INTO server_connections (license_id, connection_type, server_ip, server_port, game_port) \
|
||||||
|
VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.bind(connection_type)
|
||||||
|
.bind(server_ip)
|
||||||
|
.bind(server_port)
|
||||||
|
.bind(game_port)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update server connection details.
|
/// Update server connection status.
|
||||||
pub async fn update_server_connection(pool: &PgPool, server_id: Uuid, name: Option<&str>, host: Option<&str>, rcon_port: Option<i32>) -> Result<()> {
|
pub async fn update_connection_status(pool: &PgPool, license_id: Uuid, status: &str) -> Result<()> {
|
||||||
todo!()
|
sqlx::query(
|
||||||
|
"UPDATE server_connections SET connection_status = $1, updated_at = NOW() WHERE license_id = $2",
|
||||||
|
)
|
||||||
|
.bind(status)
|
||||||
|
.bind(license_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the initial server configuration record.
|
/// Get the server config for a license.
|
||||||
pub async fn create_server_config(pool: &PgPool, server_id: Uuid) -> Result<Uuid> {
|
pub async fn get_server_config(pool: &PgPool, license_id: Uuid) -> Result<Option<ServerConfig>> {
|
||||||
todo!()
|
let config = sqlx::query_as::<_, ServerConfig>(
|
||||||
|
"SELECT id, license_id, server_name, max_players, world_size, current_seed, \
|
||||||
|
current_map_id, server_description, server_url, server_header_image, tags, \
|
||||||
|
auto_restart_enabled, auto_restart_cron, auto_restart_timezone, \
|
||||||
|
crash_recovery_enabled, crash_recovery_max_attempts, crash_recovery_cooldown_minutes, \
|
||||||
|
force_wipe_eligible, auto_update_on_force_wipe, config_overrides, \
|
||||||
|
created_at, updated_at \
|
||||||
|
FROM server_config WHERE license_id = $1",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch server configuration.
|
/// Create a default server config for a license.
|
||||||
pub async fn get_server_config(pool: &PgPool, server_id: Uuid) -> Result<()> {
|
pub async fn create_server_config(
|
||||||
todo!()
|
pool: &PgPool,
|
||||||
|
license_id: Uuid,
|
||||||
|
server_name: &str,
|
||||||
|
) -> Result<Uuid> {
|
||||||
|
let row: (Uuid,) = sqlx::query_as(
|
||||||
|
"INSERT INTO server_config (license_id, server_name) VALUES ($1, $2) RETURNING id",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.bind(server_name)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update server configuration fields.
|
/// Update server config fields.
|
||||||
pub async fn update_server_config(pool: &PgPool, server_id: Uuid, seed: Option<i32>, world_size: Option<i32>, max_players: Option<i32>) -> Result<()> {
|
pub async fn update_server_config(
|
||||||
todo!()
|
pool: &PgPool,
|
||||||
|
license_id: Uuid,
|
||||||
|
server_name: Option<&str>,
|
||||||
|
max_players: Option<i32>,
|
||||||
|
world_size: Option<i32>,
|
||||||
|
current_seed: Option<i32>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Dynamic update — only modify provided fields
|
||||||
|
if let Some(name) = server_name {
|
||||||
|
sqlx::query("UPDATE server_config SET server_name = $1, updated_at = NOW() WHERE license_id = $2")
|
||||||
|
.bind(name)
|
||||||
|
.bind(license_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(mp) = max_players {
|
||||||
|
sqlx::query("UPDATE server_config SET max_players = $1, updated_at = NOW() WHERE license_id = $2")
|
||||||
|
.bind(mp)
|
||||||
|
.bind(license_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(ws) = world_size {
|
||||||
|
sqlx::query("UPDATE server_config SET world_size = $1, updated_at = NOW() WHERE license_id = $2")
|
||||||
|
.bind(ws)
|
||||||
|
.bind(license_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(seed) = current_seed {
|
||||||
|
sqlx::query("UPDATE server_config SET current_seed = $1, updated_at = NOW() WHERE license_id = $2")
|
||||||
|
.bind(seed)
|
||||||
|
.bind(license_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all game admins (moderators/owners) for a server.
|
/// Get all game admins for a license.
|
||||||
pub async fn get_game_admins(pool: &PgPool, server_id: Uuid) -> Result<()> {
|
pub async fn get_game_admins(pool: &PgPool, license_id: Uuid) -> Result<Vec<GameAdmin>> {
|
||||||
todo!()
|
let admins = sqlx::query_as::<_, GameAdmin>(
|
||||||
|
"SELECT id, license_id, steam_id, display_name, admin_level, permissions, added_by, created_at \
|
||||||
|
FROM game_admins WHERE license_id = $1 ORDER BY created_at",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(admins)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a game admin by Steam ID.
|
/// Add a game admin.
|
||||||
pub async fn create_game_admin(pool: &PgPool, server_id: Uuid, steam_id: &str, role: &str) -> Result<Uuid> {
|
pub async fn create_game_admin(
|
||||||
todo!()
|
pool: &PgPool,
|
||||||
|
license_id: Uuid,
|
||||||
|
steam_id: &str,
|
||||||
|
display_name: &str,
|
||||||
|
admin_level: &str,
|
||||||
|
added_by: Uuid,
|
||||||
|
) -> Result<Uuid> {
|
||||||
|
let row: (Uuid,) = sqlx::query_as(
|
||||||
|
"INSERT INTO game_admins (license_id, steam_id, display_name, admin_level, added_by) \
|
||||||
|
VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||||
|
)
|
||||||
|
.bind(license_id)
|
||||||
|
.bind(steam_id)
|
||||||
|
.bind(display_name)
|
||||||
|
.bind(admin_level)
|
||||||
|
.bind(added_by)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a game admin.
|
/// Remove a game admin.
|
||||||
pub async fn delete_game_admin(pool: &PgPool, admin_id: Uuid) -> Result<()> {
|
pub async fn delete_game_admin(pool: &PgPool, admin_id: Uuid) -> Result<()> {
|
||||||
todo!()
|
sqlx::query("DELETE FROM game_admins WHERE id = $1")
|
||||||
|
.bind(admin_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView, RouterLink, useRoute } from 'vue-router'
|
import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useServerStore } from '@/stores/server'
|
import { useServerStore } from '@/stores/server'
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const server = useServerStore()
|
const server = useServerStore()
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ function isActive(path: string): boolean {
|
|||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
auth.logout()
|
auth.logout()
|
||||||
|
router.push('/login')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { ServerConnection, ServerConfig, ServerStats } from '@/types'
|
import type { ServerConnection, ServerConfig, ServerStats } from '@/types'
|
||||||
|
import { useApi } from '@/composables/useApi'
|
||||||
|
|
||||||
export const useServerStore = defineStore('server', () => {
|
export const useServerStore = defineStore('server', () => {
|
||||||
const connection = ref<ServerConnection | null>(null)
|
const connection = ref<ServerConnection | null>(null)
|
||||||
@@ -8,28 +9,45 @@ export const useServerStore = defineStore('server', () => {
|
|||||||
const stats = ref<ServerStats | null>(null)
|
const stats = ref<ServerStats | null>(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
async function fetchServerStatus() {
|
const api = useApi()
|
||||||
// TODO: Fetch from API
|
|
||||||
|
async function fetchServer() {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await api.get<{ connection: ServerConnection | null; config: ServerConfig | null }>('/servers')
|
||||||
|
connection.value = data.connection
|
||||||
|
config.value = data.config
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch server:', e)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServerConfig() {
|
async function updateConfig(updates: Partial<ServerConfig>) {
|
||||||
// TODO: Fetch from API
|
try {
|
||||||
|
await api.put('/servers/config', updates)
|
||||||
|
await fetchServer()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to update config:', e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startServer() {
|
|
||||||
// TODO: POST /api/servers/:id/start
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopServer() {
|
|
||||||
// TODO: POST /api/servers/:id/stop
|
|
||||||
}
|
|
||||||
|
|
||||||
async function restartServer() {
|
|
||||||
// TODO: POST /api/servers/:id/restart
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendCommand(command: string) {
|
async function sendCommand(command: string) {
|
||||||
// TODO: POST /api/servers/:id/command
|
return api.post('/servers/command', { command })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startServer() {
|
||||||
|
return api.post('/servers/start')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopServer() {
|
||||||
|
return api.post('/servers/stop')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartServer() {
|
||||||
|
return api.post('/servers/restart')
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStats(newStats: ServerStats) {
|
function updateStats(newStats: ServerStats) {
|
||||||
@@ -41,12 +59,12 @@ export const useServerStore = defineStore('server', () => {
|
|||||||
config,
|
config,
|
||||||
stats,
|
stats,
|
||||||
isLoading,
|
isLoading,
|
||||||
fetchServerStatus,
|
fetchServer,
|
||||||
fetchServerConfig,
|
updateConfig,
|
||||||
|
sendCommand,
|
||||||
startServer,
|
startServer,
|
||||||
stopServer,
|
stopServer,
|
||||||
restartServer,
|
restartServer,
|
||||||
sendCommand,
|
|
||||||
updateStats,
|
updateStats,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useServerStore } from '@/stores/server'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const server = useServerStore()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
server.fetchServer()
|
||||||
|
})
|
||||||
|
|
||||||
|
function statusColor(status: string | undefined): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected': return 'bg-green-500'
|
||||||
|
case 'degraded': return 'bg-yellow-500'
|
||||||
|
default: return 'bg-red-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string | undefined): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected': return 'Online'
|
||||||
|
case 'degraded': return 'Degraded'
|
||||||
|
default: return 'Offline'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds: number | undefined): string {
|
||||||
|
if (!seconds) return '\u2014'
|
||||||
|
const h = Math.floor(seconds / 3600)
|
||||||
|
const m = Math.floor((seconds % 3600) / 60)
|
||||||
|
if (h > 0) return `${h}h ${m}m`
|
||||||
|
return `${m}m`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -20,15 +51,17 @@ const auth = useAuthStore()
|
|||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
<p class="text-sm text-neutral-400 mb-2">Server Status</p>
|
<p class="text-sm text-neutral-400 mb-2">Server Status</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
|
<span class="h-2.5 w-2.5 rounded-full" :class="statusColor(server.connection?.connection_status)"></span>
|
||||||
<span class="text-2xl font-bold text-neutral-100">Offline</span>
|
<span class="text-2xl font-bold text-neutral-100">{{ statusLabel(server.connection?.connection_status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Players Online -->
|
<!-- Players Online -->
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
<p class="text-sm text-neutral-400 mb-2">Players Online</p>
|
<p class="text-sm text-neutral-400 mb-2">Players Online</p>
|
||||||
<p class="text-2xl font-bold text-neutral-100">0/0</p>
|
<p class="text-2xl font-bold text-neutral-100">
|
||||||
|
{{ server.stats?.player_count ?? 0 }}/{{ server.stats?.max_players ?? server.config?.max_players ?? 0 }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next Wipe -->
|
<!-- Next Wipe -->
|
||||||
@@ -40,7 +73,7 @@ const auth = useAuthStore()
|
|||||||
<!-- Uptime -->
|
<!-- Uptime -->
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
<p class="text-sm text-neutral-400 mb-2">Uptime</p>
|
<p class="text-sm text-neutral-400 mb-2">Uptime</p>
|
||||||
<p class="text-2xl font-bold text-neutral-100">—</p>
|
<p class="text-2xl font-bold text-neutral-100">{{ formatUptime(server.stats?.uptime_seconds) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,24 +82,46 @@ const auth = useAuthStore()
|
|||||||
<h2 class="text-lg font-semibold text-neutral-200 mb-4">Quick Actions</h2>
|
<h2 class="text-lg font-semibold text-neutral-200 mb-4">Quick Actions</h2>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
disabled
|
:disabled="server.connection?.connection_status === 'connected'"
|
||||||
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
class="px-4 py-2.5 bg-green-600/20 hover:bg-green-600/30 disabled:opacity-30 disabled:cursor-not-allowed text-green-400 border border-green-600/30 rounded-lg text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Start Server
|
Start Server
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled
|
:disabled="server.connection?.connection_status !== 'connected'"
|
||||||
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
class="px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 disabled:opacity-30 disabled:cursor-not-allowed text-red-400 border border-red-600/30 rounded-lg text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Stop Server
|
Stop Server
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled
|
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
||||||
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
Trigger Wipe
|
Trigger Wipe
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Info (if configured) -->
|
||||||
|
<div v-if="server.config" class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
|
<h2 class="text-lg font-semibold text-neutral-200 mb-3">Server Configuration</h2>
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-neutral-500">Server Name</span>
|
||||||
|
<p class="text-neutral-200 mt-0.5">{{ server.config.server_name || 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-neutral-500">Max Players</span>
|
||||||
|
<p class="text-neutral-200 mt-0.5">{{ server.config.max_players ?? 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-neutral-500">World Size</span>
|
||||||
|
<p class="text-neutral-200 mt-0.5">{{ server.config.world_size ?? 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-neutral-500">Current Seed</span>
|
||||||
|
<p class="text-neutral-200 mt-0.5">{{ server.config.current_seed ?? 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user