diff --git a/api/app.js b/api/app.js index 82f0d44..1afacd3 100644 --- a/api/app.js +++ b/api/app.js @@ -1,6 +1,7 @@ const express = require('express'); const cors = require('cors'); const { limiter } = require('./middleware/limiter.middleware'); +const logger = require('./middleware/logger.middleware'); const app = express(); @@ -12,6 +13,7 @@ const infoRoutes = require('./routes/info.routes'); app.use(cors()); app.use(limiter) +app.use(logger); app.use(express.json()); app.use('/server', serverRoutes); diff --git a/api/controllers/info.controller.js b/api/controllers/info.controller.js index 636232e..c4b9eda 100644 --- a/api/controllers/info.controller.js +++ b/api/controllers/info.controller.js @@ -16,24 +16,8 @@ async function playerCount(req, res) { async function getUpTime(req, res) { try { - if (infoService.getStartTime() === null) { - res.status(200).send({ uptime: "0s" }); - return; - } - const ms = Date.now() - infoService.getStartTime(); - - const totalSeconds = Math.floor(ms / 1000); - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; - - let formatted = []; - if (hours > 0) formatted.push(`${hours}h`); - if (minutes > 0 || hours > 0) formatted.push(`${minutes}m`); - formatted.push(`${seconds}s`); - - res.status(200).send({ uptime: formatted.join(" ") }); + res.status(200).send(await infoService.getUpTime()); } catch (error) { console.error(error); res.status(500).send("error.. " + error.message); @@ -80,11 +64,23 @@ async function getPlatform(req, res) { } } +async function getAllInfo(req, res) { + try { + let serverProcess = serverService.getServerProcess(); + let jarPath = consts.serverDirectory + "/" + consts.serverName + res.status(200).send(await infoService.getInfo(serverProcess, jarPath, consts.serverDirectory)); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + module.exports = { playerCount, getUpTime, getMemoryUsage, getWorldSize, getVersion, - getPlatform + getPlatform, + getAllInfo } \ No newline at end of file diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js index f7182e7..64ea930 100644 --- a/api/controllers/installations.controller.js +++ b/api/controllers/installations.controller.js @@ -5,8 +5,8 @@ const { Sema } = require('async-sema'); const downloadSema = new Sema(1); async function downloadServer(req, res) { - await downloadSema.acquire(); - + await downloadSema.acquire(); + try { await installationsService.downloadRouter(req.params.platform, req.params.version); res.status(201).send('Downloaded Successfully'); diff --git a/api/controllers/properties.controller.js b/api/controllers/properties.controller.js index 72d0cd9..b4d7b22 100644 --- a/api/controllers/properties.controller.js +++ b/api/controllers/properties.controller.js @@ -6,12 +6,12 @@ const { Sema } = require('async-sema'); let togglePropertySema = new Sema(1); -async function toggleProperty(req, res) { +async function updateProperty(req, res) { togglePropertySema.acquire(); try { console.log(req.params.property); - await propertiesService.updateProperty(req.params.property, true); + await propertiesService.updateProperty(req.params.property, req.params.newvalue); res.status(200).send("done"); } catch(error) { console.error(error); @@ -151,7 +151,7 @@ async function modifyBannedIPs(req, res) { module.exports = { - toggleProperty, + updateProperty, allocateRam, serverConfig, getWhitelist, diff --git a/api/middleware/logger.middleware.js b/api/middleware/logger.middleware.js new file mode 100644 index 0000000..595966f --- /dev/null +++ b/api/middleware/logger.middleware.js @@ -0,0 +1,18 @@ +module.exports = (req, res, next) => { + const colors = { + GET: '\x1b[32m', // Green + POST: '\x1b[33m', // Yellow + PUT: '\x1b[34m', // Blue + PATCH: '\x1b[35m', + DELETE: '\x1b[31m', // Red + RESET: '\x1b[0m' // Reset color + }; + const color = colors[req.method] || colors.RESET; +// console.log( +// `${color}[${new Date().toISOString()}] ${req.method} ${req.url} - IP: ${req.ip} - User-Agent: ${req.headers['user-agent']}${colors.RESET}` +// ); + console.log( + `${color}[${new Date().toISOString()}] ${req.method} ${req.url} - IP: ${req.ip}${colors.RESET}` + ); + next(); +}; diff --git a/api/routes/info.routes.js b/api/routes/info.routes.js index efc5065..328a956 100644 --- a/api/routes/info.routes.js +++ b/api/routes/info.routes.js @@ -7,9 +7,12 @@ const { getMemoryUsage, getWorldSize, getVersion, - getPlatform + getPlatform, + getAllInfo } = require('../controllers/info.controller'); +router.get('/', getAllInfo); + router.get('/player-count', playerCount); router.get('/uptime', getUpTime); diff --git a/api/routes/properties.routes.js b/api/routes/properties.routes.js index 812026c..3f16c4d 100644 --- a/api/routes/properties.routes.js +++ b/api/routes/properties.routes.js @@ -3,7 +3,7 @@ const router = express.Router(); const propertiesServices = require('../services/properties.service'); const { - toggleProperty, + updateProperty, allocateRam, serverConfig, getWhitelist, @@ -25,7 +25,7 @@ router.get('/', async (req, res) => { } }) -router.put('/toggle/:property', toggleProperty); +router.put('/update/:property/:newvalue', updateProperty); router.put('/allocate-ram/:mb', allocateRam); diff --git a/api/services/info.service.js b/api/services/info.service.js index 149237e..c4f73e7 100644 --- a/api/services/info.service.js +++ b/api/services/info.service.js @@ -2,6 +2,9 @@ const fs = require('fs'); const path = require('path'); const pidusage = require('pidusage') const AdmZip = require('adm-zip'); +const consts = require('../consts'); +const serverService = require("./server.service"); +const propertiesService = require("./properties.service"); let startTime = null; @@ -56,13 +59,11 @@ function getDirectorySize(folderPath) { return sizeInMB; } -function getPlatform(jarPath) { +function getPlatform(jarPath = consts.serverDirectory + "/" + consts.serverName) { const zip = new AdmZip(jarPath); const entries = zip.getEntries(); const names = entries.map(e => e.entryName); - console.log(names); - const has = (file) => names.includes(file); // Detect Paper @@ -88,19 +89,20 @@ function getPlatform(jarPath) { } function getVersion(jarPath) { - if (getPlatform(jarPath) == "Fabric") { - console.error("Unable to get version from fabric servers"); - return "Unable to fetch version"; - } - + const zip = new AdmZip(jarPath); const entries = zip.getEntries(); - + + if (getPlatform(jarPath) == "Fabric") { + const installEntry = entries.find(entry => entry.entryName === 'install.properties'); + const text = zip.readAsText(installEntry); + return text.split("game-version=")[1]; + } + const versionEntry = entries.find(e => e.entryName === "version.json"); if (versionEntry) { try { const content = JSON.parse(zip.readAsText(versionEntry)); - console.log(content); return content.name || content.id || null; } catch (err) { console.warn("Failed to parse version.json:", err.message); @@ -117,6 +119,86 @@ function getVersion(jarPath) { return null; } +async function getUpTime() { + if (getStartTime() === null) + return { uptime: "0s" }; + + const ms = Date.now() - getStartTime(); + + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + let formatted = []; + if (hours > 0) formatted.push(`${hours}h`); + if (minutes > 0 || hours > 0) formatted.push(`${minutes}m`); + formatted.push(`${seconds}s`); + + return { uptime: formatted.join(" ") }; +} + +async function getInfo(serverProcess, jarPath, folderPath) { + let memoryUsage = null; + let platform = null; + let version = null; + let directorySizeMB = null; + let serverStatus = null; + let playerCount = null; + + try { + memoryUsage = await getMemoryUsage(serverProcess); + } catch (err) { + console.warn('Failed to get memory usage:', err.message); + } + + try { + platform = getPlatform(jarPath); + } catch (err) { + console.warn('Failed to get platform:', err.message); + } + + try { + version = getVersion(jarPath); + } catch (err) { + console.warn('Failed to get version:', err.message); + } + + try { + directorySizeMB = getDirectorySize(folderPath); + directorySizeMB = Math.round(directorySizeMB * 100) / 100; + } catch (err) { + console.warn('Failed to get directory size:', err.message); + } + + try { + uptime = await getUpTime(); + } catch (err) { + console.warn('Failed to calculate uptime:', err.message); + } + + try { + serverStatus = serverService.isServerStarting(); + } catch (err) { + console.warn('Failed to get server status:', err.message); + } + + try { + playerCount = await propertiesService.getOnlinePlayers(); + } catch (err) { + console.warn('Failed to get player count:', err.message); + } + + return { + memoryUsage, + platform, + version, + directorySizeMB, + uptime: uptime.uptime, + status: serverStatus, + playerCount: playerCount + }; +} module.exports = { startCounting, @@ -125,5 +207,7 @@ module.exports = { getMemoryUsage, getDirectorySize, getPlatform, - getVersion + getVersion, + getInfo, + getUpTime } \ No newline at end of file diff --git a/api/services/installations.service.js b/api/services/installations.service.js index ea691bd..1eb7ba6 100644 --- a/api/services/installations.service.js +++ b/api/services/installations.service.js @@ -1,10 +1,56 @@ -const urlFetcher = require("../utils/platformURLFetcherUtil"); +const urlFetcher = require("../utils/url_fetcher.util"); +const infoService = require('../services/info.service'); +const consts = require('../consts'); const { writeDownloadedFile } = require("../utils/installations.util"); +const fs = require('fs/promises'); +const path = require('path'); + +const preserveList = [ + "world", + "world_nether", + "world_the_end", + "banned-ips.json", + "banned-players.json", + "server.properties", + "ops.json", + "whitelist.json" +]; + +async function purgeServer(preserveList) { + let folderPath = consts.serverDirectory; + try { + const entries = await fs.readdir(folderPath, { withFileTypes: true }); + + for (const entry of entries) { + if (preserveList.includes(entry.name)) continue; + + const fullPath = path.join(folderPath, entry.name); + if (entry.isDirectory()) + await fs.rm(fullPath, { recursive: true, force: true }); + else + await fs.unlink(fullPath); + } + + console.log(`Deleted everything in ${folderPath} except preserved items.`); + } catch (err) { + console.error(`Error cleaning folder ${folderPath}:`, err.message); + } +} async function downloadRouter(platform, version) { + const oldPlatform = infoService.getPlatform(); + + const comingFromModded = oldPlatform === "fabric" || oldPlatform === "forge"; + const currentPreserveList = comingFromModded + ? preserveList.slice(3) // delete worlds + : preserveList; // keep everything + + if (oldPlatform != platform) + await purgeServer(currentPreserveList); + let response; try { - switch(platform) { + switch (platform) { case "vanilla": response = await fetch(await urlFetcher.fetchVanillaURL(version)); break; @@ -17,9 +63,10 @@ async function downloadRouter(platform, version) { case "forge": response = await fetch(await urlFetcher.fetchForgeURL(version)); break; - case _: - throw new Error(`Invalid platform --> ${platform}`) + default: + throw new Error(`Invalid platform --> ${platform}`); } + await writeDownloadedFile(response, version, platform.toUpperCase()); } catch (error) { console.error(error); diff --git a/api/services/properties.service.js b/api/services/properties.service.js index cfabece..0cefd6c 100644 --- a/api/services/properties.service.js +++ b/api/services/properties.service.js @@ -2,6 +2,7 @@ const consts = require("../consts"); const fs = require("fs"); const { getConfigAttribute } = require("../utils/config.util"); const { spawn } = require('child_process'); +const os = require('os'); async function getProperties(){ try{ @@ -71,33 +72,82 @@ function JSONToProperties(json){ } async function getOnlinePlayers() { - const port = getConfigAttribute("port"); - + const port = getConfigAttribute("mc_port"); + const platform = os.platform(); + let cmd, args; + + if (platform === "win32") { + cmd = "netstat"; + args = ["-ano"]; + } else if (platform === "linux" || platform === "darwin") { + cmd = "ss"; + args = ["-tanp"]; + } else { + console.error("Unsupported OS:", platform); + return 0; + } + return new Promise((resolve) => { - let output = ''; - - const netstat = spawn('netstat', ['-ano']); - const find = spawn('find', [`"${port}"`], { shell: true }); + let output = ""; - netstat.stdout.pipe(find.stdin); - - find.stdout.on('data', (data) => { + const proc = spawn(cmd, args); + + proc.stdout.on("data", (data) => { output += data.toString(); }); - find.on('close', (code) => { - if (code === 0 || code === 1) { // find returns 1 when no matches - const lines = output.trim().split('\n'); - const count = lines.filter(line => line.includes('ESTABLISHED')).length; - resolve(count); - } else { - console.error('find command failed with code:', code); - resolve(0); +proc.on("close", () => { + const remoteIPs = new Set(); + const lines = output.split("\n"); + + for (const line of lines) { + const isEstablished = line.includes("ESTABLISHED") || line.includes("ESTAB"); + if (!isEstablished || !line.includes(`:${port}`)) continue; + + console.log("Raw line:", line); // 🐛 DEBUG + + let remoteAddress = ""; + + if (platform === "win32") { + const parts = line.trim().split(/\s+/); + console.log("Parsed parts (Windows):", parts); // 🐛 DEBUG + if (parts.length >= 3) { + remoteAddress = parts[2].split(":")[0]; } + } else { + const parts = line.trim().split(/\s+/); + console.log("Parsed parts (Unix):", parts); // 🐛 DEBUG + + // Find the remote IP/port field + const addressField = parts.find(p => p.includes(":") && !p.includes("127.0.0.1")); + if (addressField) { + const ipPort = addressField.split(":"); + if (ipPort.length >= 2) { + remoteAddress = ipPort[0]; + } + } + } + + console.log("Remote address:", remoteAddress); // 🐛 DEBUG + + if ( + remoteAddress && + remoteAddress !== "127.0.0.1" && + remoteAddress !== "::1" && + !remoteAddress.startsWith("192.168.") && + !remoteAddress.startsWith("10.") && + !remoteAddress.startsWith("172.") + ) { + remoteIPs.add(remoteAddress); + } + } + + // console.log("Unique IPs found:", [...remoteIPs]); // 🐛 DEBUG + resolve(remoteIPs.size); }); - find.on('error', (err) => { - console.error('find command error:', err); + proc.on("error", (err) => { + console.error(`${cmd} error:`, err); resolve(0); }); }); diff --git a/api/services/server.service.js b/api/services/server.service.js index 4eab5c8..5a745a8 100644 --- a/api/services/server.service.js +++ b/api/services/server.service.js @@ -237,7 +237,7 @@ async function startServer() { const command = 'java'; const mem = configUtils.getConfigJSON()["memory"]; - const args = [`-Xmx${mem}M`, `-Xms${Number(mem) / 2}M`, '-jar', consts.serverName, 'nogui']; + const args = [`-Xmx${mem}M`, `-Xms${Number(mem.replace('M','')) / 2}M`, '-jar', consts.serverName, 'nogui']; serverProcess = spawn(command, args, { cwd: consts.serverDirectory, // Set the working directory diff --git a/api/starter.js b/api/starter.js index 9f95191..72107a0 100644 --- a/api/starter.js +++ b/api/starter.js @@ -22,10 +22,10 @@ async function startServer(api_port, mc_port) { configUtils.generateConfigFile(); console.log("sever-config generated successfully") } - const ip = await networkingUtils.getIP(local=true) - console.log("local-ip:", ip); - + if(debug === false){ + const ip = await networkingUtils.getIP(local=true) + console.log("local-ip:", ip); await networkingUtils.forwardPort(api_port, ip); await networkingUtils.forwardPort(3000, ip); // port forwarding for frontend diff --git a/api/utils/config.util.js b/api/utils/config.util.js index a1c460b..dd419e7 100644 --- a/api/utils/config.util.js +++ b/api/utils/config.util.js @@ -4,7 +4,7 @@ const { configFilePath } = require("../consts"); const defaultConfig = { "os": os.type(), - "memory": "1024M", + "memory": "1024", "platform": "vanilla", "version": "1.21.4", "start_with_script": false, diff --git a/front-end/package-lock.json b/front-end/package-lock.json index d844aae..bb56533 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -18,6 +18,7 @@ "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.3", + "cross-env": "^7.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-scripts": "5.0.1", @@ -6494,6 +6495,24 @@ "node": ">=10" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/front-end/package.json b/front-end/package.json index 7efc49e..f67e6eb 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -13,13 +13,14 @@ "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.3", + "cross-env": "^7.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, "scripts": { - "start": "PORT=3006 react-scripts start", + "start": "cross-env PORT=3006 react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/front-end/src/App.js b/front-end/src/App.js index d5026c6..21094db 100644 --- a/front-end/src/App.js +++ b/front-end/src/App.js @@ -4,6 +4,8 @@ import 'bootstrap/dist/js/bootstrap.min.js'; import Navbar from './components/Navbar'; import ServerWindow from './components/ServerWindow'; +import { ServerDataProvider } from './utils/serverDataContext'; + function App() { @@ -11,17 +13,9 @@ function App() { return (
- - {/*
-
- - - - - - -
-
*/} + + +
diff --git a/front-end/src/components/Console.js b/front-end/src/components/Console.js index 249b84f..b0d9034 100644 --- a/front-end/src/components/Console.js +++ b/front-end/src/components/Console.js @@ -1,17 +1,34 @@ import { useEffect , useState} from "react"; import styles from '../styles/Console.module.css' -import { useServerStatus, getServerStatus } from "../utils/monitor"; +import { useServerData } from "../utils/serverDataContext"; import TurnLeftIcon from '@mui/icons-material/TurnLeft'; +const statusCodes = { + OFFLINE: 0, + ONLINE: 1, + STARTING: 2, + FETCHING: 3, + ERROR: 4 +} + function Console(){ const [consoleText, setConsoleText] = useState("The server is offline..."); const [inputText, setInputText] = useState(""); - const isServerOnline = useServerStatus(); const [isSendingCommand, setIsSendingCommand] = useState(false); + const data = useServerData(); + const [serverStatus, setServerStatus] = useState(statusCodes.FETCHING); + + useEffect(() => { + if (data?.status !== undefined) { + setServerStatus(data.status); + } else { + setServerStatus(statusCodes.ERROR); + } + }, [data]); useEffect(() => { const interval = setInterval( async () => { - if(await getServerStatus() === 1 || await getServerStatus() === 2){ + if(serverStatus === statusCodes.ONLINE || serverStatus === statusCodes.STARTING){ setIsSendingCommand(true); const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/console-text`) const text = await response.text() @@ -28,13 +45,13 @@ function Console(){ setIsSendingCommand(false); }, 2000) return () => clearInterval(interval); - }, []) + }, [serverStatus]) const handleSubmit = async (e) => { e.preventDefault(); if (inputText.trim() === "") return; - if(await getServerStatus()){ + if(serverStatus === statusCodes.ONLINE){ const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/console/run/${inputText}`, { method: 'PUT', headers: { diff --git a/front-end/src/components/InfoTab.js b/front-end/src/components/InfoTab.js new file mode 100644 index 0000000..68c93c8 --- /dev/null +++ b/front-end/src/components/InfoTab.js @@ -0,0 +1,114 @@ +import React from 'react'; +import { Card, CardContainer, CardItem, CardGrid } from './card'; +import styles from '../styles/InfoTab.module.css'; +import { useServerData } from '../utils/serverDataContext'; +import { useEffect } from 'react'; + +const statusCodes = { + STARTING: 2, + ONLINE: 1, + OFFLINE: 0 +} + +const InfoTab = () => { + const data = useServerData() || {}; + + return ( +
+ + {/* Server Information */} + + + + {data?.status === statusCodes.OFFLINE && } + {data?.status === statusCodes.ONLINE && } + {data?.status === statusCodes.STARTING && } + {' '} + { + data?.status === statusCodes.OFFLINE ? 'Offline' : + data?.status === statusCodes.ONLINE ? 'Online' : + data?.status === statusCodes.STARTING ? 'Starting' : + 'Unknown' + } + + } + /> + + + + + + + + + {/* System Resources */} + + + + + + + + {/* World Info */} + + + + + + + + +
+ ); +}; + +export default InfoTab; diff --git a/front-end/src/components/Navbar.js b/front-end/src/components/Navbar.js index f0e01e8..b401927 100644 --- a/front-end/src/components/Navbar.js +++ b/front-end/src/components/Navbar.js @@ -1,24 +1,64 @@ -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'bootstrap/dist/js/bootstrap.min.js'; +import React from 'react'; +import styles from '../styles/Navbar.module.css'; +import { useServerData } from '../utils/serverDataContext'; function Navbar(){ + const data = useServerData() || {} + + const handleShareClick = () => { + const ip = localStorage.getItem('ipAddress'); + const port = localStorage.getItem('port'); + + if (ip && port) { + let baseUrl = window.location.origin + window.location.pathname; + const frontendPort = baseUrl.split(':')[2] + const shareUrl = `http://${ip}:${frontendPort}?ip=${ip}&port=${port}`; + navigator.clipboard.writeText(shareUrl) + .then(() => { + alert("Share link copied to clipboard!"); + }) + .catch(err => { + console.error('Failed to copy share link: ', err); + alert("Failed to copy share link."); + }); + } else { + alert("Could not generate share link. IP and/or Port not found in local storage."); + } + }; + + const handleIpCopyClick = () => { + const ip = localStorage.getItem('ipAddress'); + const port = data?.server_port; + + if (ip && port) { + const serverAddress = `${ip}:${port}`; + navigator.clipboard.writeText(serverAddress) + .then(() => { + alert(`Copied "${serverAddress}" to clipboard!`); + }) + .catch(err => { + console.error('Failed to copy IP address: ', err); + alert("Failed to copy IP address."); + }); + } + }; + + const serverIp = localStorage.getItem('ipAddress'); + const serverPort = data?.server_port; + return( - + ); } diff --git a/front-end/src/components/PropertiesTab.js b/front-end/src/components/PropertiesTab.js new file mode 100644 index 0000000..3705411 --- /dev/null +++ b/front-end/src/components/PropertiesTab.js @@ -0,0 +1,229 @@ +import React, { useState } from 'react' +import { Card, CardContainer, CardItem, CardGrid } from './card' +import styles from '../styles/PropertiesTab.module.css' +import { useServerData } from '../utils/serverDataContext' +import Switch from './Switch' +import serverProperties from '../server.properties.json' + +const Property = ({ name, description, value, onChange, type, disabled }) => { + const [localValue, setLocalValue] = useState(value) + + React.useEffect(() => { + setLocalValue(value) + }, [value]) + + const handleChange = (newValue) => { + setLocalValue(newValue) + onChange(newValue) + } + + if (type === 'boolean') { + return ( +
+
+
+ {name} +
+ handleChange(checked.toString())} + /> +
+ {description} +
+ ) + } + + if (type === 'difficulty') { + return ( +
+
+
+ {name} +
+ +
+ {description} +
+ ) + } + + if (type === 'gamemode') { + return ( +
+
+
+ {name} +
+ +
+ {description} +
+ ) + } + + if (type === 'number') { + return ( +
+
+
+ {name} +
+ handleChange(Math.max(1, parseInt(e.target.value)))} + disabled={disabled} + /> +
+ {description} +
+ ) + } + + if (type === 'text') { + return ( +
+
+
+ {name} +
+ handleChange(e.target.value)} + disabled={disabled} + /> +
+ {description} +
+ ) + } + + return null +} + +const PropertiesTab = () => { + const data = useServerData() || {} + const [showAdvanced, setShowAdvanced] = useState(false) + const [loading, setLoading] = useState(false) + const [updatedProperties, setUpdatedProperties] = useState({}) + + const handlePropertyChange = (property) => async (newValue) => { + setLoading(true) + setUpdatedProperties(prev => ({ ...prev, [property]: newValue })) + try { + const response = await fetch(`http://${localStorage.getItem('ipAddress')}:${localStorage.getItem('port')}/properties/update/${property}/${newValue}`, { method: 'PUT' }) + if (!response.ok) { + throw new Error(`Failed to set property ${property} to ${newValue}`) + } + } catch (error) { + console.error(`Error setting property ${property} to ${newValue}: ${error}`) + } + finally{ + setLoading(false) + } + } + + const importantProperties = [ + 'pvp', + 'difficulty', + 'gamemode', + 'hardcore', + 'online-mode', + 'max-players', + 'white-list', + 'announce-player-achievements', + 'force-gamemode', + ] + + const getPropertyType = (key) => { + if (key === 'gamemode') return 'gamemode' + if (key === 'difficulty') return 'difficulty' + if (key === 'max-players' || key === 'view-distance' || key === 'spawn-protection' || key === 'server-port') return 'number' + if (key === 'motd' || key === 'level-name' || key === 'level-seed' || key === 'resource-pack' || key === 'resource-pack-sha1') return 'text' + return 'boolean' + } + + const isImportantProperty = (key) => importantProperties.includes(key) + + const getPropertyValue = (key) => { + const snakeKey = key.replace(/-/g, '_') + if (updatedProperties[key] !== undefined) { + return updatedProperties[key] + } + return data[snakeKey] !== undefined && data[snakeKey] !== null ? data[snakeKey].toString() : "" + } + + return ( +
+ + + + {Object.entries(serverProperties['server-properties']) + .filter(([key]) => isImportantProperty(key)) + .map(([key, value]) => ( + + ))} + + + +
+ Advanced Properties + + + {Object.entries(serverProperties['server-properties']) + .filter(([key]) => !isImportantProperty(key)) + .map(([key, value]) => ( + + ))} + + +
+
+
+ ) +} + +export default PropertiesTab diff --git a/front-end/src/components/ServerWindow.js b/front-end/src/components/ServerWindow.js index 4a5a139..4232c57 100644 --- a/front-end/src/components/ServerWindow.js +++ b/front-end/src/components/ServerWindow.js @@ -6,51 +6,57 @@ import { useState } from "react"; import VersionSelectDropdown from './VersionSelectDropdown'; import StartStopBtn from './StartStopBtn'; import Console from './Console'; -import TabButtons from './TabButtons'; - -function ServerWindow(){ - - const [tabOption, setTabOption] = useState("info-tab") - - return( -
-{/* -
- +import InfoTab from './InfoTab'; +import VersionTab from './VersionTab'; +import PropertiesTab from './PropertiesTab'; + +import Tabs from './Tabs/Tabs'; +import TabPanel from './Tabs/TabPanel'; +import { ServerDataProvider } from '../utils/serverDataContext'; + +function ServerWindow() { + return ( + +
-
- info tab placeholder -
-
- properties tab placeholder -
-
- version tab placeholder +
+ + + + + + + + + + +
-
*/} - -
- - - - {/* */} +
+ + + {/* */} +
-
+
); } - export default ServerWindow; diff --git a/front-end/src/components/StartStopBtn.js b/front-end/src/components/StartStopBtn.js index 2535e65..9d2b121 100644 --- a/front-end/src/components/StartStopBtn.js +++ b/front-end/src/components/StartStopBtn.js @@ -1,25 +1,28 @@ import { useEffect, useState } from "react"; import { forwardRef, useImperativeHandle, useRef } from "react"; -import { useServerStatus, getServerStatus } from "../utils/monitor"; +import { useServerData } from "../utils/serverDataContext"; +import styles from "../styles/StartStopBtn.module.css"; +const statusCodes = { + OFFLINE: 0, + ONLINE: 1, + STARTING: 2, + FETCHING: 3, + ERROR: 4 +} function StartStopBtn(){ + - const [serverStatus, setServerStatus] = useState(3); + const [serverStatus, setServerStatus] = useState(statusCodes.FETCHING); + const data = useServerData(); useEffect(() => { - const interval = setInterval( async () => { - if(await getServerStatus() === 1){ - setServerStatus(1); - } - else if(await getServerStatus() === 2){ - setServerStatus(2); - } - else{ - setServerStatus(0); - }; - }, 2000) - return () => clearInterval(interval); - }, []) + if (data?.status !== undefined) { + setServerStatus(data.status); + } else { + setServerStatus(statusCodes.ERROR); + } + }, [data]); const modalRef = useRef(); @@ -28,21 +31,22 @@ function StartStopBtn(){ }; async function startStopServer(){ - try{ - if(serverStatus){ - await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/stop`, {method: "PUT"}) - setServerStatus(0); + try{ + if(serverStatus === statusCodes.ONLINE){ + await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/stop`, {method: "PUT"}) + setServerStatus(statusCodes.OFFLINE); } - else{ - const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/start`, {method: "PUT"}) + else if(serverStatus === statusCodes.OFFLINE){ + const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/start`, {method: "PUT"}) if ((await response.text()).includes("EULA")){ handleOpenModal(); return; } - setServerStatus(2); + setServerStatus(statusCodes.STARTING); } } catch(error){ + alert("Failed to start/stop server, API might be offline"); console.error(error); } } @@ -53,20 +57,17 @@ function StartStopBtn(){ ) } - export default StartStopBtn; - const SimpleModal = forwardRef((props, ref) => { const dialogRef = useRef(null); @@ -91,9 +92,9 @@ const SimpleModal = forwardRef((props, ref) => { }; return ( - +

To continue you must accept Mojang's EULA

-
+
@@ -101,7 +102,7 @@ const SimpleModal = forwardRef((props, ref) => { ); }); -const styles = { +const modalStyles = { dialog: { padding: "1.5em", border: "none", diff --git a/front-end/src/components/Switch.js b/front-end/src/components/Switch.js new file mode 100644 index 0000000..997289c --- /dev/null +++ b/front-end/src/components/Switch.js @@ -0,0 +1,28 @@ +import React from 'react'; +import styles from '../styles/Switch.module.css'; + +const Switch = ({ checked, onChange }) => { + const handleChange = (e) => { + // Only allow changes if not in null state + if (checked !== null) { + onChange(e.target.checked); + } + }; + + const isDisabled = checked === null; + const isChecked = checked === true; + + return ( + + ); +}; + +export default Switch; diff --git a/front-end/src/components/Tabs/Tab.js b/front-end/src/components/Tabs/Tab.js new file mode 100644 index 0000000..57ff8c2 --- /dev/null +++ b/front-end/src/components/Tabs/Tab.js @@ -0,0 +1,14 @@ +import styles from './Tabs.module.css'; + +function Tab({ label, value, active, onClick }) { + return ( + + ); +} + +export default Tab; diff --git a/front-end/src/components/Tabs/TabPanel.js b/front-end/src/components/Tabs/TabPanel.js new file mode 100644 index 0000000..4ec3cec --- /dev/null +++ b/front-end/src/components/Tabs/TabPanel.js @@ -0,0 +1,11 @@ +import styles from './Tabs.module.css'; + +function TabPanel({ value, children }) { + return ( +
+ {children} +
+ ); +} + +export default TabPanel; diff --git a/front-end/src/components/Tabs/Tabs.js b/front-end/src/components/Tabs/Tabs.js new file mode 100644 index 0000000..3e53285 --- /dev/null +++ b/front-end/src/components/Tabs/Tabs.js @@ -0,0 +1,31 @@ +import styles from './Tabs.module.css'; +import { useState } from 'react'; +import Tab from './Tab'; +import TabPanel from './TabPanel'; + +function Tabs({ children, labels }) { + const [activeTab, setActiveTab] = useState(labels[0].value); + + return ( +
+
+ {labels.map(tab => ( + setActiveTab(tab.value)} + /> + ))} +
+
+ {children.map(child => + child.props.value === activeTab ? child : null + )} +
+
+ ); +} + +export default Tabs; diff --git a/front-end/src/components/Tabs/Tabs.module.css b/front-end/src/components/Tabs/Tabs.module.css new file mode 100644 index 0000000..b5978e9 --- /dev/null +++ b/front-end/src/components/Tabs/Tabs.module.css @@ -0,0 +1,41 @@ +.tabsContainer { + width: 100%; +} + +.tabHeader { + display: flex; + justify-content: center; + margin-bottom: 10px; +} + +.tabButton { + background: none; + border: none; + font-size: 16px; + padding: 10px 20px; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s ease-in-out; +} + +.tabButton:hover { + color: #d63384; +} + +.tabButtonActive { + border-color: #d63384; + font-weight: bold; +} + +.tabPanels { + padding: 10px; +} + +.tabPanel { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/front-end/src/components/VersionTab.js b/front-end/src/components/VersionTab.js new file mode 100644 index 0000000..0ad398c --- /dev/null +++ b/front-end/src/components/VersionTab.js @@ -0,0 +1,138 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContainer } from './card'; +import { useServerData } from '../utils/serverDataContext'; +import styles from '../styles/VersionTab.module.css'; + +const VersionTab = () => { + const data = useServerData(); + const [loading, setLoading] = useState(false); + const [selectedPlatform, setSelectedPlatform] = useState(data.platform || 'vanilla'); + const [selectedVersion, setSelectedVersion] = useState(data.version || ''); + const [availableVersions, setAvailableVersions] = useState([]); + + const platforms = ['vanilla', 'paper', 'fabric', 'forge']; + + useEffect(() => { + const fetchVersions = async () => { + try { + let versionsUrl; + switch (selectedPlatform) { + case 'vanilla': + versionsUrl = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; + const vanillaResponse = await fetch(versionsUrl); + const vanillaData = await vanillaResponse.json(); + const vanillaVersions = vanillaData.versions.map(v => v.id).filter(v => !v.includes('snapshot')); + setAvailableVersions(vanillaVersions); + if (!data.version) setSelectedVersion(vanillaVersions[0]); + break; + case 'paper': + versionsUrl = 'https://api.papermc.io/v2/projects/paper'; + const paperResponse = await fetch(versionsUrl); + const paperData = await paperResponse.json(); + setAvailableVersions(paperData.versions); + if (!data.version) setSelectedVersion(paperData.versions[0]); + break; + case 'fabric': + versionsUrl = 'https://meta.fabricmc.net/v2/versions/game'; + const fabricResponse = await fetch(versionsUrl); + const fabricData = await fabricResponse.json(); + const fabricVersions = fabricData.map(v => v.version).filter(v => !v.includes('snapshot')); + setAvailableVersions(fabricVersions); + if (!data.version) setSelectedVersion(fabricVersions[0]); + break; + case 'forge': + versionsUrl = 'https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json'; + const forgeResponse = await fetch(versionsUrl); + const forgeData = await forgeResponse.json(); + const forgeVersions = Object.keys(forgeData); + setAvailableVersions(forgeVersions); + if (!data.version) setSelectedVersion(forgeVersions[0]); + break; + } + } catch (error) { + console.error('Error fetching versions:', error); + setAvailableVersions([]); + } + }; + + fetchVersions(); + }, [selectedPlatform, data.version]); + + useEffect(() => { + if (availableVersions.length > 0 && !availableVersions.includes(selectedVersion)) { + setSelectedVersion(availableVersions[0]); + } + }, [availableVersions, selectedVersion]); + + const handleDownload = async () => { + setLoading(true); + try { + const response = await fetch( + `http://${localStorage.getItem('ipAddress')}:${localStorage.getItem('port')}/installations/download/${selectedPlatform}/${selectedVersion}`, + { method: 'PUT' } + ); + if (!response.ok) { + throw new Error('Failed to download server'); + } + } catch (error) { + console.error('Error downloading server:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ +
+

Current Platform: {data.platform || 'Not installed'}

+

Current Version: {data.version || 'Not installed'}

+
+
+
+
+ ); +}; + +export default VersionTab; \ No newline at end of file diff --git a/front-end/src/components/card.js b/front-end/src/components/card.js new file mode 100644 index 0000000..d83169c --- /dev/null +++ b/front-end/src/components/card.js @@ -0,0 +1,40 @@ + +import React from 'react'; +import styles from '../styles/Card.module.css'; + +export const Card = ({ title, children }) => { + return ( +
+ {title &&

{title}

} +
+ {children} +
+
+ ); +}; + +export const CardContainer = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export const CardItem = ({ label, value }) => { + return ( +
+ {label} + {value} +
+ ); +}; + +export const CardGrid = ({ children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/front-end/src/index.js b/front-end/src/index.js index f045733..0cd8e78 100644 --- a/front-end/src/index.js +++ b/front-end/src/index.js @@ -6,24 +6,34 @@ import App from './App'; import { useState } from 'react'; import { Box, TextField, Button } from '@mui/material'; +// Read URL parameters and apply them to localStorage +const urlParams = new URLSearchParams(window.location.search); +const ipFromURL = urlParams.get('ip'); +const portFromURL = urlParams.get('port'); +if (ipFromURL && portFromURL) { + localStorage.setItem('ipAddress', ipFromURL); + localStorage.setItem('port', portFromURL); -const IpForm = () => { + // Optional: Clean the URL after applying params + const cleanURL = new URL(window.location); + cleanURL.search = ''; + window.history.replaceState({}, document.title, cleanURL.toString()); +} +const IpForm = () => { + const [checkingURL, setCheckingURL] = useState(false); + const [ipAddress, setIpAddress] = useState(''); + const [port, setPort] = useState(''); async function checkURL(ipAddress, port) { try { setCheckingURL(true); const isIPv4 = ipAddress !== "localhost" ? /^(\d{1,3}\.){3}\d{1,3}$/.test(ipAddress) : "localhost"; - // console.log(`http://${isIPv4 ? ipAddress : `[${ipAddress}]`}:${port}/ping`); const response = await fetch(`http://${isIPv4 ? ipAddress : `[${ipAddress}]`}:${port}/ping`); setCheckingURL(false); - if (response.ok) { - return true; - } else { - return false; - } + return response.ok; } catch (error) { console.error(error); setCheckingURL(false); @@ -31,21 +41,20 @@ const IpForm = () => { } } - - const [checkingURL, setCheckingURL] = useState(false); - const [ipAddress, setIpAddress] = useState(''); - const [port, setPort] = useState(''); - const handleSubmit = async (e) => { e.preventDefault(); - if (await checkURL(ipAddress, port)){ + if (await checkURL(ipAddress, port)) { + if(ipAddress === 'localhost') { + const publicIp = await fetch('https://ifconfig.me/ip') + const result = await checkURL(publicIp, port); + if(result === 200) + ipAddress = publicIp; + } localStorage.setItem('ipAddress', ipAddress); localStorage.setItem('port', port); window.location.reload(); - } - else{ + } else { alert("Invalid IP Address or Port, try again"); - } }; @@ -78,20 +87,16 @@ const IpForm = () => { Submit - ) -} - - + ); +}; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - {(!localStorage.getItem('ipAddress') || !localStorage.getItem('port')) ? ( - - ) : ( - - )} + {(!localStorage.getItem('ipAddress') || !localStorage.getItem('port')) ? ( + + ) : ( + + )} ); - - diff --git a/front-end/src/server.properties.json b/front-end/src/server.properties.json new file mode 100644 index 0000000..f057c9c --- /dev/null +++ b/front-end/src/server.properties.json @@ -0,0 +1,249 @@ + +{ + "server-properties": { + "accepts-transfers": { + "default": false, + "description": "Controls whether server accepts player transfers" + }, + "allow-flight": { + "default": false, + "description": "Allows players to fly in survival mode if set to true" + }, + "allow-nether": { + "default": true, + "description": "Allows players to travel to the Nether" + }, + "broadcast-console-to-ops": { + "default": true, + "description": "Send console output to all online operators" + }, + "broadcast-rcon-to-ops": { + "default": true, + "description": "Send RCON output to all online operators" + }, + "bug-report-link": { + "default": "", + "description": "Link for submitting bug reports" + }, + "difficulty": { + "default": "easy", + "description": "Sets game difficulty (easy/normal/hard/peaceful)" + }, + "enable-command-block": { + "default": false, + "description": "Enables command blocks" + }, + "enable-jmx-monitoring": { + "default": false, + "description": "Enables JMX monitoring" + }, + "enable-query": { + "default": false, + "description": "Enables GameSpy4 protocol server listener" + }, + "enable-rcon": { + "default": false, + "description": "Enables remote access to server console" + }, + "enable-status": { + "default": true, + "description": "Makes server appear in server list" + }, + "enforce-secure-profile": { + "default": true, + "description": "Enforces secure profile settings" + }, + "enforce-whitelist": { + "default": false, + "description": "Enforces whitelist on the server" + }, + "entity-broadcast-range-percentage": { + "default": 100, + "description": "Controls how far entities are broadcast to players" + }, + "force-gamemode": { + "default": false, + "description": "Forces players to join in the default game mode" + }, + "function-permission-level": { + "default": 2, + "description": "Permission level for function commands" + }, + "gamemode": { + "default": "survival", + "description": "Default game mode for new players" + }, + "generate-structures": { + "default": true, + "description": "Defines whether structures generate in world" + }, + "generator-settings": { + "default": "{}", + "description": "Settings used to customize world generation" + }, + "hardcore": { + "default": false, + "description": "If true, players are banned upon death" + }, + "hide-online-players": { + "default": false, + "description": "Hides online players in server status" + }, + "initial-disabled-packs": { + "default": "", + "description": "Data packs disabled by default" + }, + "initial-enabled-packs": { + "default": "vanilla", + "description": "Data packs enabled by default" + }, + "level-name": { + "default": "world", + "description": "The name of the world" + }, + "level-seed": { + "default": "", + "description": "Seed for world generation" + }, + "level-type": { + "default": "minecraft:normal", + "description": "Defines the type of world to generate" + }, + "log-ips": { + "default": true, + "description": "Log player IP addresses" + }, + "max-chained-neighbor-updates": { + "default": 1000000, + "description": "Maximum number of chained neighbor updates" + }, + "max-players": { + "default": 20, + "description": "Maximum number of players allowed" + }, + "max-tick-time": { + "default": 60000, + "description": "Maximum milliseconds per tick before server timeout" + }, + "max-world-size": { + "default": 29999984, + "description": "Maximum world size in blocks" + }, + "motd": { + "default": "A Minecraft Server", + "description": "Message displayed in server list" + }, + "network-compression-threshold": { + "default": 256, + "description": "Limits network compression threshold in bytes" + }, + "online-mode": { + "default": true, + "description": "Requires players to be authenticated with Minecraft account" + }, + "op-permission-level": { + "default": 4, + "description": "Sets permission level for operators" + }, + "pause-when-empty-seconds": { + "default": 60, + "description": "Time in seconds before pausing empty server" + }, + "player-idle-timeout": { + "default": 0, + "description": "Time in minutes before idle players are kicked" + }, + "prevent-proxy-connections": { + "default": false, + "description": "Prevents connections through proxies" + }, + "pvp": { + "default": true, + "description": "Enables player vs player combat" + }, + "query.port": { + "default": 25565, + "description": "Port for GameSpy4 protocol server listener" + }, + "rate-limit": { + "default": 0, + "description": "Sets the rate limit for connections" + }, + "rcon.password": { + "default": "", + "description": "Password for RCON" + }, + "rcon.port": { + "default": 25575, + "description": "Port for RCON" + }, + "region-file-compression": { + "default": "deflate", + "description": "Compression type for region files" + }, + "require-resource-pack": { + "default": false, + "description": "Forces clients to use server resource pack" + }, + "resource-pack": { + "default": "", + "description": "URL to resource pack" + }, + "resource-pack-id": { + "default": "", + "description": "Unique identifier for resource pack" + }, + "resource-pack-prompt": { + "default": "", + "description": "Message shown when resource pack is offered" + }, + "resource-pack-sha1": { + "default": "", + "description": "SHA1 hash of resource pack" + }, + "server-ip": { + "default": "", + "description": "IP address to bind server to" + }, + "server-port": { + "default": 25565, + "description": "Port the server listens on" + }, + "simulation-distance": { + "default": 10, + "description": "Maximum simulation distance in chunks" + }, + "spawn-monsters": { + "default": true, + "description": "Determines if monsters can spawn" + }, + "spawn-protection": { + "default": 16, + "description": "Radius of spawn area protection" + }, + "sync-chunk-writes": { + "default": true, + "description": "Enables synchronous chunk writes" + }, + "text-filtering-config": { + "default": "", + "description": "Configuration for text filtering" + }, + "text-filtering-version": { + "default": 0, + "description": "Version of text filtering to use" + }, + "use-native-transport": { + "default": true, + "description": "Uses native transport if available" + }, + "view-distance": { + "default": 10, + "description": "Maximum view distance in chunks" + }, + "white-list": { + "default": false, + "description": "Enables server whitelist" + } + } +} diff --git a/front-end/src/styles/Card.module.css b/front-end/src/styles/Card.module.css new file mode 100644 index 0000000..a8c359d --- /dev/null +++ b/front-end/src/styles/Card.module.css @@ -0,0 +1,62 @@ + +.card { + + background: var(--code-bg-light); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 20px; + margin-bottom: 20px; +} + +.cardTitle { + font-size: 1.5rem; + margin-bottom: 15px; + color: #333; +} + +.cardContent { + width: 100%; +} + +.cardContainer { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 10px; + overflow-y: auto; + +} + +.cardItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--secondary); +} + +.cardLabel { + font-weight: 500; + color: #0c0c0c; +} + +.cardValue { + color: var(--code-bg); +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + color: #0c0c0c; + gap: 20px; + width: 100%; +} + +.truncatedText { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100px; /* or whatever fits your layout */ + display: inline-block; + vertical-align: bottom; +} \ No newline at end of file diff --git a/front-end/src/styles/Console.module.css b/front-end/src/styles/Console.module.css index 619e691..0b15a55 100644 --- a/front-end/src/styles/Console.module.css +++ b/front-end/src/styles/Console.module.css @@ -1,5 +1,5 @@ .viewbox{ - background-color: var(--secondary); + background-color: var(--code-bg); width: 100%; padding-top: 1em; @@ -17,7 +17,7 @@ } .consoleTextBox{ - background-color: var(--secondary); + background-color: var(--code-bg); width: 100%; padding-left: 1em; @@ -35,14 +35,14 @@ .consoleInputField{ - background-color: var(--light-secondary); + background-color: var(--code-bg-light); padding: 0.25em 0.25em 0.25em 0.25em; border-radius: 1em; width: 95%; color: white; } .consoleInputButton{ - background-color: var(--light-secondary); + background-color: var(--code-bg-light); padding: 0.25em 0.25em 0.25em 0.25em; border-radius: 1em; color: black; diff --git a/front-end/src/styles/InfoTab.module.css b/front-end/src/styles/InfoTab.module.css index 208aad2..133bda4 100644 --- a/front-end/src/styles/InfoTab.module.css +++ b/front-end/src/styles/InfoTab.module.css @@ -1,4 +1,6 @@ .card{ background-color: var(--element); color: var(--text); -} \ No newline at end of file +} + + diff --git a/front-end/src/styles/Navbar.module.css b/front-end/src/styles/Navbar.module.css new file mode 100644 index 0000000..4cf3f49 --- /dev/null +++ b/front-end/src/styles/Navbar.module.css @@ -0,0 +1,61 @@ +.navbar { + background-color: rgb(36, 36, 36); + width: 100%; + position: absolute; + top: 0; + padding: 0.5rem 1rem; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +} + +.shareButton { + background-color: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); /* A light color, similar to bootstrap's 'light' */ + color: #f8f9fa; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + cursor: pointer; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out; + font-family: inherit; /* Ensure button inherits font */ +} + +.shareButton:hover { + color: #212529; /* A dark color for text on hover */ + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.shareButton:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.ipDisplay { + color: #f8f9fa; + background-color: rgba(0, 0, 0, 0.2); + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.15s ease-in-out; + display: flex; + align-items: center; + font-family: inherit; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.ipDisplay:hover { + background-color: rgba(0, 0, 0, 0.4); +} + +.ipDisplay code { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + background-color: rgba(0, 0, 0, 0.4); + padding: 0.2rem 0.4rem; + border-radius: 0.2rem; + margin-left: 0.5rem; +} diff --git a/front-end/src/styles/PropertiesTab.module.css b/front-end/src/styles/PropertiesTab.module.css new file mode 100644 index 0000000..3558d92 --- /dev/null +++ b/front-end/src/styles/PropertiesTab.module.css @@ -0,0 +1,129 @@ +.container { + padding: 0; + margin: 0; + overflow-y: auto; + width: 100%; + +} + +.property { + padding: 5px; + border-radius: 8px; + background-color: var(--code-accent); + margin-bottom: 10px; +} + +.propertyHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.propertyName { + font-weight: 600; + font-size: 1rem; + color: #333; + margin-bottom: 8px; +} + +.propertyDescription { + font-size: 0.6rem; + color: var(--code-text); + line-height: 1.3; + margin-bottom: 6px; +} + +.container::-webkit-scrollbar { + width: 8px; +} + +.container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.container::-webkit-scrollbar-thumb { + background: #007bff; + border-radius: 4px; +} + +.container::-webkit-scrollbar-thumb:hover { + background: #0056b3; +} + +.property { + display: flex; + flex-direction: column; + padding: 10px; + background-color: var(--code-accent); + border-radius: 4px; +} + +.propertyHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.propertyName { + font-weight: 500; +} + +.select { + position: relative; + display: inline-block; + width: 120px; + height: 34px; + padding: 0 10px; + border: 1px solid black; + border-radius: 4px; + background-color: var(--code-accent); + cursor: pointer; + transition: .4s; +} + +.select:focus { + outline: none; + border-color: #2196F3; + box-shadow: 0 0 1px #2196F3; +} + +.numberInput { + position: relative; + display: inline-block; + width: 80px; + height: 34px; + padding: 0 10px; + border: 1px solid black; + border-radius: 4px; + background-color: var(--code-accent); + transition: .4s; +} + +.numberInput:focus { + outline: none; + border-color: #2196F3; + box-shadow: 0 0 1px #2196F3; +} + +.container { + padding: 20px; +} + +.advancedProperties { + margin-top: 20px; +} + +.advancedProperties summary { + cursor: pointer; + padding: 10px; + color: #333; + background-color: var(--code-accent); + border-radius: 4px; + font-weight: 500; +} + +.advancedProperties summary:hover { + background-color: var(--code-accent); +} diff --git a/front-end/src/styles/ServerWindow.module.css b/front-end/src/styles/ServerWindow.module.css index ffcc0e7..43987bf 100644 --- a/front-end/src/styles/ServerWindow.module.css +++ b/front-end/src/styles/ServerWindow.module.css @@ -5,6 +5,7 @@ justify-content: center; min-width: 50em; max-width: 60em; + max-height: 50em; gap: 1em; width: 100%; /* Make the sub-container take up the full width of its parent */ padding: 2em 2em 2em 2em; @@ -14,25 +15,26 @@ display: flex; min-height: 37em; color: aliceblue; - background-color: var(--secondary); + background-color: var(--code-bg); outline: 0.25em solid var(--dark-third); border-radius: 1em; margin-left: 2em; min-width: 55em; - + max-height: 90%; + overflow-y: auto; } .container { display: flex; - min-height: 80%; + max-height: 43em; border-radius: 0.5em; align-items: center; justify-content: center; flex-direction: row; width:100%; - height: 100%; + overflow: hidden; gap: 3px; outline: 1px solid; outline-color: var(--primary); diff --git a/front-end/src/styles/StartStopBtn.module.css b/front-end/src/styles/StartStopBtn.module.css new file mode 100644 index 0000000..50c2e87 --- /dev/null +++ b/front-end/src/styles/StartStopBtn.module.css @@ -0,0 +1,95 @@ +.button { + position: relative; + display: inline-block; + margin: 20px; + width: 100%; + font-family: Helvetica, sans-serif; + font-weight: bold; + font-size: 36px; + text-align: center; + border: none; + padding: 20px 40px; + color: white; + text-shadow: 0px 1px 0px #000; + border-radius: 5px; + transition: background-color 0.3s; +} + +.button:active { + top: 10px; +} + +.button:after { + content: ""; + height: 100%; + width: 100%; + padding: 4px; + position: absolute; + bottom: -15px; + left: -4px; + z-index: -1; + border-radius: 5px; +} + +.success { + background-color: #28a745; + cursor: pointer; + color: white; + box-shadow: inset 0 1px 0 #4caf50, 0 10px 0 #1e7e34; +} + +.success:active { + background-color: #218838; + box-shadow: inset 0 1px 0 #4caf50, inset 0 -3px 0 #1e7e34; +} + +.success:after { + background-color: #145523; +} + +.danger { + background-color: #dc3545; + cursor: pointer; + color: white; + box-shadow: inset 0 1px 0 #e4606d, 0 10px 0 #bd2130; +} + +.danger:active { + background-color: #c82333; + box-shadow: inset 0 1px 0 #e4606d, inset 0 -3px 0 #bd2130; +} + +.danger:after { + background-color: #921925; +} +.warning { + background-color: #f0ad4e; /* Bootstrap warning base */ + cursor: not-allowed; + color: white; + box-shadow: inset 0 1px 0 #ffc878, 0 10px 0 #d98c0a; +} + +.warning:active { + background-color: #ec971f; + box-shadow: inset 0 1px 0 #ffc878, inset 0 -3px 0 #d98c0a; +} + +.warning:after { + background-color: #b76b00; +} + +.secondary { + background-color: #6c757d; + cursor: not-allowed; + color: white; + box-shadow: inset 0 1px 0 #adb5bd, 0 10px 0 #545b62; +} + +.secondary:active { + background-color: #5a6268; + box-shadow: inset 0 1px 0 #adb5bd, inset 0 -3px 0 #545b62; +} + +.secondary:after { + background-color: #343a40; +} diff --git a/front-end/src/styles/Switch.module.css b/front-end/src/styles/Switch.module.css new file mode 100644 index 0000000..12f89e3 --- /dev/null +++ b/front-end/src/styles/Switch.module.css @@ -0,0 +1,70 @@ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ff4444; + transition: .4s; + border-radius: 34px; +} + +.slider:before { + position: absolute; + content: "✕"; + display: flex; + align-items: center; + justify-content: center; + color: #ff4444; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #4CAF50; +} + +input:focus + .slider { + box-shadow: 0 0 1px #4CAF50; +} + +input:checked + .slider:before { + content: "✓"; + color: #4CAF50; + transform: translateX(26px); +} + +/* Disabled/null state styles */ +.switch.disabled .slider { + background-color: #cccccc; + cursor: not-allowed; +} + +.switch.disabled .slider:before { + content: "—"; + color: #888888; + transform: translateX(13px); /* Center the slider */ +} + +.switch.disabled .slider:hover { + background-color: #cccccc; /* Prevent hover effects */ +} diff --git a/front-end/src/styles/VersionTab.module.css b/front-end/src/styles/VersionTab.module.css new file mode 100644 index 0000000..b7585dc --- /dev/null +++ b/front-end/src/styles/VersionTab.module.css @@ -0,0 +1,73 @@ + +/* VersionTab.module.css */ +.container { + width: 100%; + height: 100%; +} + +.versionSelector { + display: flex; + flex-direction: column; + gap: 20px; +} + +.selectorGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.selectorGroup label { + font-weight: 500; + color: #333; +} + +.select { + padding: 8px 12px; + border-radius: 4px; + border: 1px solid #000000; + background-color: var(--code-accent); + font-size: 14px; + width: 100%; +} + +.select:disabled { + background-color: #2c2c2c; + cursor: not-allowed; +} + +.downloadButton { + padding: 10px 20px; + background-color: #4CAF50; + color: var(--code-bg-light); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.downloadButton:hover { + background-color: #45a049; +} + +.downloadButton:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.currentVersion { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid black; +} + +.currentVersion p { + margin: 8px 0; + color: #666; +} + +.currentVersion span { + font-weight: 500; + color: #333; +} diff --git a/front-end/src/styles/theme.css b/front-end/src/styles/theme.css index 9407d2a..61488d9 100644 --- a/front-end/src/styles/theme.css +++ b/front-end/src/styles/theme.css @@ -8,6 +8,10 @@ --element: #8BB174; --light-element: #B5CA8D; --text-color: #000000; + --code:#d63384; + --code-bg: #381e2b; + --code-bg-light:#61384d; + --code-accent: #8b4e6d; } [data-theme='dark'] { diff --git a/front-end/src/utils/monitor.js b/front-end/src/utils/monitor.js deleted file mode 100644 index cab2173..0000000 --- a/front-end/src/utils/monitor.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useState, useEffect } from "react"; - -export const useServerStatus = () => { - const [serverStatus, setServerStatus] = useState(false); - - useEffect(() => { - const interval = setInterval(async () => { - try { - const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/check-server`); - const data = await response.json(); - setServerStatus(data); - } catch (error) { - console.error("Failed to fetch server status:", error); - setServerStatus(false); - } - }, 2000); - - return () => clearInterval(interval); - }, []); - - return serverStatus; -}; - -export const getServerStatus = async () => { - try { - const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/check-server`); - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch server status:", error); - return false; - } -} \ No newline at end of file diff --git a/front-end/src/utils/serverDataContext.js b/front-end/src/utils/serverDataContext.js new file mode 100644 index 0000000..ca3beff --- /dev/null +++ b/front-end/src/utils/serverDataContext.js @@ -0,0 +1,179 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; + +const ServerDataContext = createContext(null); +export const ServerDataProvider = ({ children }) => { + const [data, setData] = useState({ + memoryUsage: { + cpu: "0%", + usedMB: "0", + }, + platform: null, + version: null, + directorySizeMB: null, + uptime: null, + allow_nether: null, + broadcast_console_to_ops: null, + broadcast_rcon_to_ops: null, + difficulty: null, + enable_command_block: null, + enable_jmx_monitoring: null, + enable_rcon: null, + enable_status: null, + enforce_whitelist: null, + entity_broadcast_range_percentage: null, + force_gamemode: null, + function_permission_level: null, + gamemode: null, + generate_structures: null, + hardcore: null, + hide_online_players: null, + level_name: null, + level_seed: null, + level_type: null, + max_players: null, + max_tick_time: null, + max_world_size: null, + motd: null, + network_compression_threshold: null, + online_mode: null, + op_permission_level: null, + player_idle_timeout: null, + prevent_proxy_connections: null, + pvp: null, + query_port: null, + rate_limit: null, + rcon_password: null, + rcon_port: null, + require_resource_pack: null, + resource_pack: null, + resource_pack_prompt: null, + resource_pack_sha1: null, + server_ip: null, + server_port: null, + simulation_distance: null, + spawn_animals: null, + spawn_monsters: null, + spawn_npcs: null, + spawn_protection: null, + sync_chunk_writes: null, + text_filtering_config: null, + use_native_transport: null, + view_distance: null, + white_list: null + }); + + const toSnakeCase = str => str.replace(/-/g, '_').replace(/\./g, '_'); + + + useEffect(() => { + console.log("Updated context data:", data); + }, [data]); + + const normalizeKeys = obj => + Object.fromEntries( + Object.entries(obj).map(([key, value]) => [toSnakeCase(key), value]) + ); + + useEffect(() => { + const fetchData = async () => { + try { + const [infores, propertiesResRaw] = await Promise.all([ + fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/info/`).then(res => res.json()), + fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/properties/`).then(res => res.json()), + ]); + + const propertiesRes = normalizeKeys(propertiesResRaw); + + + // console.log("infores:", infores); + // console.log("propertiesResRaw:", propertiesResRaw); + // console.log("normalized propertiesRes:", propertiesRes); + // console.log("data:", data); + + setData(prev => ({ + ...prev, + ...infores, + ...propertiesRes + })); + } catch (error) { + console.error("Fetch error:", error); + const resetAllToNull = () => { + setData({ + memoryUsage: { + cpu: "0%", + usedMB: "0", + }, + platform: null, + version: null, + directorySizeMB: 0, + uptime: null, + allow_nether: null, + broadcast_console_to_ops: null, + broadcast_rcon_to_ops: null, + difficulty: null, + enable_command_block: null, + enable_jmx_monitoring: null, + enable_rcon: null, + enable_status: null, + enforce_whitelist: null, + entity_broadcast_range_percentage: null, + force_gamemode: null, + function_permission_level: null, + gamemode: null, + generate_structures: null, + hardcore: null, + hide_online_players: null, + level_name: null, + level_seed: null, + level_type: null, + max_players: null, + max_tick_time: null, + max_world_size: null, + motd: null, + network_compression_threshold: null, + online_mode: null, + op_permission_level: null, + player_idle_timeout: null, + prevent_proxy_connections: null, + pvp: null, + query_port: null, + rate_limit: null, + rcon_password: null, + rcon_port: null, + require_resource_pack: null, + resource_pack: null, + resource_pack_prompt: null, + resource_pack_sha1: null, + server_ip: null, + server_port: null, + simulation_distance: null, + spawn_animals: null, + spawn_monsters: null, + spawn_npcs: null, + spawn_protection: null, + sync_chunk_writes: null, + text_filtering_config: null, + use_native_transport: null, + view_distance: null, + white_list: null + }); + }; + resetAllToNull(); + } + }; + + + fetchData(); + const interval = setInterval(fetchData, 2500); + + return () => clearInterval(interval); + }, []); + + return ( + + {children} + + ); +}; + +export const useServerData = () => useContext(ServerDataContext); diff --git a/installer.bat b/installer.bat new file mode 100644 index 0000000..1e09423 --- /dev/null +++ b/installer.bat @@ -0,0 +1,40 @@ +@echo off +setlocal +echo === Running setup scripts... === + +REM Navigate to the scripts\windows directory +cd scripts\windows || ( + echo ❌ Error: scripts\windows not found + pause + exit /b 1 +) + +REM === Check and set up Minecraft server === +echo [1/4] Running check_minecraft_server.bat... +call check_minecraft_server.bat +IF ERRORLEVEL 1 ( + echo ❌ check_minecraft_server.bat failed + pause + goto :EOF +) + +REM === Install dependencies === +echo [2/4] Running install_dependencies.bat... +call install_dependencies.bat +IF ERRORLEVEL 1 ( + echo ❌ install_dependencies.bat failed + pause + goto :EOF +) + +REM Return to project root (2 levels up) +cd ..\.. || ( + echo ❌ Failed to return to project root + pause + goto :EOF +) + +echo ✅ All scripts completed successfully. +:EOF +pause +exit /b \ No newline at end of file diff --git a/installer.exe b/installer.exe new file mode 100755 index 0000000..e4f0775 Binary files /dev/null and b/installer.exe differ diff --git a/installer.py b/installer.py new file mode 100644 index 0000000..c341788 --- /dev/null +++ b/installer.py @@ -0,0 +1,333 @@ +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +import subprocess +import os +import sys +import threading +import json +import urllib.request +from pathlib import Path +import ctypes +import platform + +class InstallerGUI: + def __init__(self, root): + self.root = root + self.servers_running = False + self.server_processes = [] + self.root.title("Navarch Installer & Server Manager") + self.root.geometry("800x600") + self.root.resizable(True, True) + + if self.is_windows() and not self.is_admin(): + self.request_admin() + return + + self.setup_ui() + + def is_windows(self): + return platform.system() == "Windows" + + def is_linux(self): + return platform.system() == "Linux" + + def is_admin(self): + if self.is_windows(): + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except: + return False + elif self.is_linux(): + return os.geteuid() == 0 + return True + + def request_admin(self): + if self.is_windows(): + try: + ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, " ".join(sys.argv), None, 1 + ) + except: + messagebox.showerror("Error", "Administrator privileges required!") + finally: + self.root.quit() + elif self.is_linux(): + messagebox.showerror("Error", "Please run this script as root using: sudo python3 installer.py") + self.root.quit() + + def setup_ui(self): + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(4, weight=1) + + title_label = ttk.Label(main_frame, text="Navarch Installer & Server Manager", + font=("Arial", 16, "bold")) + title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20)) + + buttons_frame = ttk.Frame(main_frame) + buttons_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E)) + for i in range(3): + buttons_frame.columnconfigure(i, weight=1) + + self.install_btn = ttk.Button(buttons_frame, text="🔧 Install Dependencies", + command=self.run_installer, width=20) + self.install_btn.grid(row=0, column=0, padx=(0, 5), sticky=(tk.W, tk.E)) + + self.start_btn = ttk.Button(buttons_frame, text="🚀 Start Navarch", command=self.toggle_servers) + self.start_btn.grid(row=0, column=1, padx=5, sticky=(tk.W, tk.E)) + + self.close_btn = ttk.Button(buttons_frame, text="❌ Close", command=self.close_app) + self.close_btn.grid(row=0, column=2, padx=(5, 0), sticky=(tk.W, tk.E)) + + self.progress = ttk.Progressbar(main_frame, mode='indeterminate') + self.progress.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + self.status_label = ttk.Label(main_frame, text="Ready to install") + self.status_label.grid(row=3, column=0, columnspan=2, pady=(0, 10)) + + self.output_text = scrolledtext.ScrolledText(main_frame, height=20, width=80) + self.output_text.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S)) + + self.output_text.tag_configure("success", foreground="green") + self.output_text.tag_configure("error", foreground="red") + self.output_text.tag_configure("info", foreground="blue") + self.output_text.tag_configure("warning", foreground="orange") + + def log_output(self, message, tag="normal"): + self.output_text.insert(tk.END, f"{message}\n", tag) + self.output_text.see(tk.END) + self.root.update_idletasks() + + def run_command(self, command, cwd=None): + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + cwd=cwd + ) + return result.returncode == 0, result.stdout, result.stderr + except Exception as e: + return False, "", str(e) + + def run_and_log(self, command, success_msg, error_msg): + success, _, stderr = self.run_command(command) + if success: + self.log_output(f"✅ {success_msg}", "success") + else: + self.log_output(f"❌ {error_msg}: {stderr}", "error") + return success + + def check_node_js(self): + self.log_output("Checking for Node.js...", "info") + success, stdout, _ = self.run_command("node --version") + if success: + self.log_output(f"✅ Node.js is installed: {stdout.strip()}", "success") + return True + else: + self.log_output("❌ Node.js is not installed", "error") + return False + + def install_node_js(self): + if self.is_windows(): + self.log_output("Installing Node.js...", "info") + return self.run_and_log( + "winget install --silent --accept-package-agreements --accept-source-agreements OpenJS.NodeJS", + "Node.js installed successfully", + "Failed to install Node.js" + ) + else: + self.log_output("Installing Node.js for Linux...", "info") + return self.run_and_log( + "sudo pacman -Sy --noconfirm nodejs npm", + "Node.js and NPM installed", + "Failed to install Node.js on Linux" + ) + + def check_java(self): + self.log_output("Checking for Java JDK...", "info") + success, stdout, _ = self.run_command("java --version") + if success: + for line in stdout.splitlines(): + if 'version' in line.lower(): + try: + version_str = line.split()[1].strip('"') + major_version = int(version_str.split('.')[0]) + if major_version >= 21: + self.log_output(f"✅ Java JDK {version_str} is installed", "success") + return True + else: + self.log_output(f"⚠️ Java version {version_str} is too old (need 21+)", "warning") + return False + except: + pass + self.log_output("❌ Java JDK 21+ is not installed", "error") + return False + + def install_java(self): + if self.is_windows(): + self.log_output("Installing OpenJDK 21...", "info") + return self.run_and_log( + "winget install EclipseAdoptium.Temurin.21.JDK --silent --accept-package-agreements --accept-source-agreements", + "OpenJDK 21 installed successfully", + "Failed to install OpenJDK 21" + ) + else: + self.log_output("Installing OpenJDK 21 for Linux...", "info") + return self.run_and_log( + "sudo pacman -Sy --noconfirm jdk-openjdk", + "OpenJDK installed", + "Failed to install OpenJDK 21 on Linux" + ) + + def check_minecraft_server(self): + self.log_output("Checking for Minecraft server JAR...", "info") + server_path = Path("server/server.jar") + + if server_path.exists(): + self.log_output("✅ server.jar already exists", "success") + return True + + self.log_output("server.jar not found. Setting up Minecraft server...", "warning") + Path("server").mkdir(exist_ok=True) + + try: + with urllib.request.urlopen("https://launchermeta.mojang.com/mc/game/version_manifest.json") as response: + version_data = json.loads(response.read().decode()) + + latest_version = version_data["latest"]["release"] + version_info = next(v for v in version_data["versions"] if v["id"] == latest_version) + + with urllib.request.urlopen(version_info["url"]) as response: + version_json = json.loads(response.read().decode()) + + server_jar_url = version_json["downloads"]["server"]["url"] + urllib.request.urlretrieve(server_jar_url, server_path) + + if server_path.exists(): + self.log_output("✅ Minecraft server.jar downloaded successfully", "success") + return True + except Exception as e: + self.log_output(f"❌ Failed to download server.jar: {str(e)}", "error") + return False + + def install_npm_dependencies(self): + self.log_output("Installing NPM dependencies...", "info") + + for name, path in [("front-end", "front-end"), ("back-end", "api")]: + if not Path(f"{path}/node_modules").exists(): + self.log_output(f"Installing {name} dependencies...", "info") + success, _, stderr = self.run_command("npm install", cwd=path) + if not success: + self.log_output(f"❌ Failed to install {name} dependencies: {stderr}", "error") + return False + self.log_output(f"✅ {name.capitalize()} dependencies installed", "success") + else: + self.log_output(f"✅ {name.capitalize()} dependencies already installed", "success") + + self.log_output("Installing serve globally...", "info") + success, _, stderr = self.run_command("npm install -g serve") + if success: + self.log_output("✅ Serve installed globally", "success") + else: + self.log_output(f"⚠️ Warning: Failed to install serve globally: {stderr}", "warning") + if self.is_linux(): + messagebox.showwarning( + "Permission Error", + "Could not install 'serve' globally due to permissions. You can install it manually with:\n\n sudo npm install -g serve" + ) + + return True + + def run_installer_thread(self): + try: + self.progress.start() + self.status_label.config(text="Installing dependencies...") + self.install_btn.config(state="disabled") + + self.log_output("=== Running setup scripts... ===", "info") + + if not self.check_node_js() and not self.install_node_js(): + raise Exception("Failed to install Node.js") + + if not self.check_java() and not self.install_java(): + raise Exception("Failed to install Java JDK") + + if not self.check_minecraft_server(): + raise Exception("Failed to setup Minecraft server") + + if not self.install_npm_dependencies(): + raise Exception("Failed to install NPM dependencies") + + self.log_output("✅ All scripts completed successfully!", "success") + self.status_label.config(text="Installation completed successfully") + messagebox.showinfo("Success", "Installation completed successfully!") + + except Exception as e: + self.log_output(f"❌ Installation failed: {str(e)}", "error") + self.status_label.config(text="Installation failed") + messagebox.showerror("Error", f"Installation failed: {str(e)}") + finally: + self.progress.stop() + self.install_btn.config(state="normal") + + def run_installer(self): + threading.Thread(target=self.run_installer_thread, daemon=True).start() + + def toggle_servers(self): + if self.servers_running: + self.stop_servers() + else: + threading.Thread(target=self.start_servers_thread, daemon=True).start() + + def start_servers_thread(self): + try: + self.progress.start() + self.status_label.config(text="Starting servers...") + self.start_btn.config(state="disabled") + + self.log_output("🚀 Starting backend server...", "info") + backend_proc = subprocess.Popen("npm start", cwd="api", shell=True) + + self.log_output("🚀 Starting frontend server...", "info") + frontend_proc = subprocess.Popen("serve -s build", cwd="front-end", shell=True) + + self.server_processes = [backend_proc, frontend_proc] + self.servers_running = True + + self.start_btn.config(text="🛑 Stop Navarch") + self.status_label.config(text="Servers running") + self.log_output("✅ Servers started", "success") + except Exception as e: + self.log_output(f"❌ Failed to start servers: {str(e)}", "error") + self.status_label.config(text="Server start failed") + finally: + self.progress.stop() + self.start_btn.config(state="normal") + + def stop_servers(self): + self.log_output("Stopping servers...", "info") + for proc in self.server_processes: + if proc.poll() is None: + proc.terminate() + self.server_processes.clear() + self.servers_running = False + self.start_btn.config(text="🚀 Start Navarch") + self.status_label.config(text="Servers stopped") + self.log_output("✅ Servers stopped", "success") + + def close_app(self): + if self.servers_running: + self.stop_servers() + self.root.destroy() + + +if __name__ == '__main__': + root = tk.Tk() + app = InstallerGUI(root) + root.mainloop() diff --git a/run_windows.bat b/run_windows.bat deleted file mode 100644 index 5644349..0000000 --- a/run_windows.bat +++ /dev/null @@ -1,45 +0,0 @@ -@echo off -setlocal -echo Running setup scripts... - -REM Navigate to the appropriate scripts directory -cd scripts\windows || ( - echo scripts\windows not found - exit /b 1 -) - -REM Check and set up Minecraft server -call check_minecraft_server.bat -IF ERRORLEVEL 1 ( - echo check_minecraft_server.bat failed - exit /b 1 -) - -REM Run dependency installation script -call install_dependencies.bat -IF ERRORLEVEL 1 ( - echo install_dependencies.bat failed - exit /b 1 -) - -REM Run npm install script -call npm_install.bat -IF ERRORLEVEL 1 ( - echo npm_install.bat failed - exit /b 1 -) - -REM Start servers -call start_servers.bat -IF ERRORLEVEL 1 ( - echo start_servers.bat failed - exit /b 1 -) - -cd ..\.. || ( - echo Failed to return to project root - exit /b 1 -) - -echo All scripts completed successfully. -pause diff --git a/scripts/linux_debian/npm_install.sh b/scripts/linux_debian/npm_install.sh index 802103a..7f486d0 100755 --- a/scripts/linux_debian/npm_install.sh +++ b/scripts/linux_debian/npm_install.sh @@ -1,22 +1,22 @@ -#!/bin/bash +@echo off -cd .. -cd .. +echo Starting backend server... -# Check if front-end dependencies are missing -if [ ! -d "front-end/node_modules" ]; then - echo "Installing front-end dependencies..." - cd front-end - npm install - cd .. -fi +start cmd /k "cd ..\..\api && npm start" -# Check if back-end dependencies are missing -if [ ! -d "api/node_modules" ]; then - echo "Installing back-end dependencies..." - cd api - npm install - cd .. -fi - -cd scripts +echo === Checking frontend === +if exist "..\..\front-end\build" ( + echo Frontend build folder exists. Serving... + start "" cmd /k "cd ..\..\front-end\build && npx serve" +) else ( + echo Frontend build folder not found. Building... + pushd ..\..\front-end + npm run build + popd + if exist "..\..\front-end\build" ( + echo Build complete. Serving frontend... + start "" cmd /k "cd ..\..\front-end\build && npx serve" + ) else ( + echo Frontend build failed. + ) +) diff --git a/scripts/linux_debian/start_servers.sh b/scripts/linux_debian/start_servers.sh index d0a9c70..8c66cb3 100755 --- a/scripts/linux_debian/start_servers.sh +++ b/scripts/linux_debian/start_servers.sh @@ -1,7 +1,7 @@ #!/bin/bash # List of common terminal emulators -TERMINALS=("gnome-terminal" "xfce4-terminal" "konsole" "xterm" "lxterminal" "tilix" "mate-terminal" "kitty") +TERMINALS=("gnome-terminal" "xfce4-terminal" "konsole" "xterm" "lxterminal" "tilix" "mate-terminal" "kitty" "alacritty" "wezterm") # Find a supported terminal for term in "${TERMINALS[@]}"; do @@ -19,5 +19,21 @@ fi echo "Starting backend server..." "$TERMINAL" -- bash -c "cd ../../api && npm start; exec bash" & -echo "Starting frontend server..." -"$TERMINAL" -- bash -c "cd ../../front-end && npm start; exec bash" & +echo "=== Checking frontend build ===" +if [ -d "../../front-end/build" ]; then + echo "Frontend build folder exists. Serving..." + "$TERMINAL" -- bash -c "cd ../../front-end/build && npx serve; exec bash" & +else + echo "Frontend build folder not found. Building..." + ( + cd ../../front-end || exit 1 + npm run build + ) + + if [ -d "../../front-end/build" ]; then + echo "Build complete. Serving frontend..." + "$TERMINAL" -- bash -c "cd ../../front-end/build && npx serve; exec bash" & + else + echo "Frontend build failed." + fi +fi \ No newline at end of file diff --git a/scripts/windows/check_minecraft_server.bat b/scripts/windows/check_minecraft_server.bat index 695f675..07b73d7 100644 --- a/scripts/windows/check_minecraft_server.bat +++ b/scripts/windows/check_minecraft_server.bat @@ -6,27 +6,16 @@ IF NOT EXIST "..\..\server\server.jar" ( REM Create server directory mkdir "..\..\server" - IF ERRORLEVEL 1 ( - echo Failed to create server directory - exit /b 1 - ) echo Fetching latest Minecraft version info... - powershell -Command ^ - "$versionData = Invoke-RestMethod 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; ^ - $latest = $versionData.latest.release; ^ - $versionInfo = $versionData.versions | Where-Object { $_.id -eq $latest }; ^ - if (-not $versionInfo) { Write-Error 'Could not find version info'; exit 1 }; ^ - $versionJson = Invoke-RestMethod $versionInfo.url; ^ - $serverJarUrl = $versionJson.downloads.server.url; ^ - echo Latest version: $latest; ^ - echo Downloading server.jar from $serverJarUrl...; ^ - Invoke-WebRequest $serverJarUrl -OutFile '..\..\server\server.jar'; ^ - echo Minecraft server.jar downloaded successfully." ^ - || ( + powershell -Command "$versionData = Invoke-RestMethod 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; $latest = $versionData.latest.release; $versionInfo = $versionData.versions | Where-Object { $_.id -eq $latest }; if (-not $versionInfo) { Write-Error 'Could not find version info'; exit 1 }; $versionJson = Invoke-RestMethod $versionInfo.url; $serverJarUrl = $versionJson.downloads.server.url; echo Latest version: $latest; echo Downloading server.jar from $serverJarUrl...; Invoke-WebRequest $serverJarUrl -OutFile '..\..\server\server.jar'; if (-not (Test-Path '..\..\server\server.jar')) { Write-Error 'Download failed'; exit 1 }" + + IF ERRORLEVEL 1 ( echo Failed to fetch or download server.jar exit /b 1 ) + + echo Minecraft server.jar downloaded successfully. ) ELSE ( echo server.jar already exists. -) +) \ No newline at end of file diff --git a/scripts/windows/install_dependencies.bat b/scripts/windows/install_dependencies.bat index 6053622..ad22dbc 100644 --- a/scripts/windows/install_dependencies.bat +++ b/scripts/windows/install_dependencies.bat @@ -1,65 +1,60 @@ @echo off +setlocal enabledelayedexpansion echo Checking dependencies... +echo. +:: ------------------------------- :: Check Node.js +:: ------------------------------- +echo Checking for Node.js... where node >nul 2>&1 if %ERRORLEVEL% EQU 0 ( - echo Node.js is installed + echo Node.js is installed. node --version ) else ( echo Node.js is not installed. Installing... - curl -o node-installer.msi https://nodejs.org/dist/v20.10.0/node-v20.10.0-x64.msi - msiexec /i node-installer.msi /qn - del node-installer.msi - echo Node.js has been installed + winget install --silent --accept-package-agreements --accept-source-agreements OpenJS.NodeJS ) +echo. -:: Check NPM -where npm >nul 2>&1 +:: ------------------------------- +:: Check Java JDK (21+) +:: ------------------------------- +echo Checking for JDK... +where java >nul 2>&1 if %ERRORLEVEL% EQU 0 ( - echo NPM is installed - npm --version + echo JDK is installed. + java -version ) else ( - echo NPM is not installed. Installing... - :: NPM comes with Node.js, but if somehow it's missing - curl -o npm.ps1 https://npmjs.org/install.ps1 - powershell.exe -ExecutionPolicy Bypass -File npm.ps1 - del npm.ps1 - echo NPM has been installed -) + for /f "tokens=3" %%i in ('java --version 2^>^&1 ^| findstr /i "version"') do set JAVA_VERSION=%%i + set JAVA_VERSION=!JAVA_VERSION:"=! + for /f "delims=. tokens=1" %%a in ("!JAVA_VERSION!") do set JAVA_MAJOR=%%a -:: Check OpenJDK 21 -where java >nul 2>&1 -if %ERRORLEVEL% EQU 0 ( - for /f "tokens=3" %%i in ('java -version 2^>^&1 ^| findstr /i "version"') do set JAVA_VERSION=%%i - set JAVA_VERSION=%JAVA_VERSION:"=% - for /f "delims=. tokens=1" %%a in ("%JAVA_VERSION%") do set JAVA_MAJOR=%%a if !JAVA_MAJOR! GEQ 21 ( - echo OpenJDK 21 or above is installed - java -version + echo Java JDK !JAVA_VERSION! is installed. + java --version ) else ( - echo OpenJDK 21 or above is not installed. Installing... - curl -o jdk21.zip https://download.java.net/java/GA/jdk21.0.1/415e3f918a1f4062a0074a2794853d0d/12/GPL/openjdk-21.0.1_windows-x64_bin.zip - powershell.exe -Command "Expand-Archive -Path jdk21.zip -DestinationPath 'C:\Program Files\Java'" - del jdk21.zip - :: Set JAVA_HOME environment variable - setx JAVA_HOME "C:\Program Files\Java\jdk-21.0.1" /M - :: Add to PATH - setx PATH "%PATH%;%JAVA_HOME%\bin" /M - echo OpenJDK 21 has been installed + echo Detected Java version: !JAVA_VERSION! + echo Java JDK 21 or above is not installed. + call :InstallJava ) +) +goto :eof + +:: ------------------------------- +:: Java Install Function +:: ------------------------------- +:InstallJava +echo Installing OpenJDK 21... +start /wait "" winget install EclipseAdoptium.Temurin.21.JDK --silent --accept-package-agreements --accept-source-agreements +if %ERRORLEVEL% EQU 0 ( + echo OpenJDK 21 has been installed successfully. ) else ( - echo Java is not installed. Installing... - curl -o jdk21.zip https://download.java.net/java/GA/jdk21.0.1/415e3f918a1f4062a0074a2794853d0d/12/GPL/openjdk-21.0.1_windows-x64_bin.zip - powershell.exe -Command "Expand-Archive -Path jdk21.zip -DestinationPath 'C:\Program Files\Java'" - del jdk21.zip - :: Set JAVA_HOME environment variable - setx JAVA_HOME "C:\Program Files\Java\jdk-21.0.1" /M - :: Add to PATH - setx PATH "%PATH%;%JAVA_HOME%\bin" /M - echo OpenJDK 21 has been installed + echo Failed to install OpenJDK 21. ) +goto :eof + @REM :: Check OpenJDK 17 @REM where java >nul 2>&1 diff --git a/scripts/windows/npm_install.bat b/scripts/windows/npm_install.bat index 898ca1e..7feee94 100644 --- a/scripts/windows/npm_install.bat +++ b/scripts/windows/npm_install.bat @@ -1,4 +1,5 @@ cd .. +cd .. if not exist "front-end\node_modules" ( echo Installing front-end dependencies... @@ -9,9 +10,12 @@ if not exist "front-end\node_modules" ( if not exist "api\node_modules" ( echo Installing back-end dependencies... - cd back-end + cd api call npm install cd .. ) +call npm install -g serve + cd scripts +cd windows \ No newline at end of file diff --git a/scripts/windows/start_servers.bat b/scripts/windows/start_servers.bat deleted file mode 100644 index 6870993..0000000 --- a/scripts/windows/start_servers.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off - -echo Starting backend server... - -start cmd /k "cd ..\..\api && npm start" - -echo Starting frontend server... - -start cmd /k "cd ..\..\front-end && npm start" \ No newline at end of file diff --git a/start_servers.bat b/start_servers.bat new file mode 100644 index 0000000..3599736 --- /dev/null +++ b/start_servers.bat @@ -0,0 +1,66 @@ +@echo off +setlocal enabledelayedexpansion + +:: Check if Node.js is installed +where node >nul 2>&1 +if NOT %ERRORLEVEL% EQU 0 ( + echo ❌ Node.js is not installed. + echo Please run installer.bat to install required packages. + pause + goto :eof +) + +echo === Checking backend dependencies === +if not exist "api\node_modules" ( + echo ⚙️ Backend dependencies missing. Running npm_install.bat... + pushd scripts\windows + call npm_install.bat + if ERRORLEVEL 1 ( + echo ❌ npm_install.bat failed for backend + pause + popd + goto :eof + ) + popd +) + +echo === Checking frontend dependencies === +if not exist "front-end\node_modules" ( + echo ⚙️ Frontend dependencies missing. Running npm_install.bat... + pushd scripts\windows + call npm_install.bat + if ERRORLEVEL 1 ( + echo ❌ npm_install.bat failed for frontend + pause + popd + goto :eof + ) + popd +) + +echo ✅ All dependencies installed. + +echo Starting backend server... +start "" cmd /k "cd api && npm start" + +echo === Checking frontend === +if exist "front-end\build" ( + echo 🟢 Frontend build folder exists. Serving... + start "" cmd /k "cd front-end\build && npx serve" +) else ( + echo 🔧 Frontend build folder not found. Building... + pushd front-end + npm run build + popd + if exist "front-end\build" ( + echo ✅ Build complete. Serving frontend... + start "" cmd /k "cd front-end\build && npx serve" + ) else ( + echo ❌ Frontend build failed. + pause + ) +) + +echo ✅ You can now close this window. +pause +endlocal \ No newline at end of file