diff --git a/README.md b/README.md index b15c095..08f216f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ FeaturesServer SoftwareGetting Started • - Web Panel • + Screenshots • + Web Panel • + Map PageDevelopmentRoadmap

@@ -44,11 +46,24 @@ A Turkish project summary is available near the end of this README: [Türkçe Ö ## 📌 Project Status -**Current package version:** `0.1.0` +**Current package version:** `0.2.0` -MSMS is under active development. The desktop server-management core is implemented, while some newer modules — especially the **Web Panel** and **Store / Economy** — should be treated as **beta** features. +MSMS is under active development, and the whole of it now runs: the desktop +manager, the **admin web panel**, the **public website with its store and +economy**, and a **third page that is nothing but a fullscreen live map**. The +web layer and the store are no longer beta — they are covered by the same +automated gates as the rest of the app. -Before using the project for an important public server, test the workflows you plan to rely on and keep independent backups of your server data. +What is still young rather than unfinished: + +- **MSMS Bridge** — the plugin builds and self-tests, and the app reads it, but + it has had far less time in live servers than the desktop core. +- **Modded worlds** — block colours and item icons come from the vanilla client + jar, so a heavily modded world renders its vanilla blocks correctly and falls + back to a generated colour for the rest. + +Before using MSMS for an important public server, test the workflows you plan to +rely on and keep independent backups of your server data. --- @@ -85,6 +100,70 @@ MSMS aims to cover both everyday administration and more advanced server-managem --- +## 📸 Screenshots + +Every picture below is taken by the app of itself against throwaway demo +servers — `MSMS_SHOTS=` renders each view offscreen and writes a PNG, so +they can be retaken after a redesign instead of quietly going stale. + +### Dashboard + +Live CPU, memory, TPS, players and uptime, with the launch arguments and the +exact command MSMS will run underneath them. + +![Dashboard](docs/screenshots/dashboard.png) + +### Creating a server + +Server software grouped by what it is *for* — vanilla, plugin servers, modded, +hybrid, proxy — rather than an alphabetical list of names a newcomer cannot rank. + +![Create a server](docs/screenshots/create-server.png) + +### `server.properties` + +Every setting with an explanation, grouped and searchable, plus raw editing for +anything MSMS does not know about. + +![Server properties](docs/screenshots/properties.png) + +### Players and the live map + +Moderation, world controls, the NBT inventory viewer, and a live map rendered +from the server's own region files. + +![Players](docs/screenshots/players.png) + +### Plugins and mods + +Installed jars with enable/disable, plus Modrinth search and one-click install. + +![Plugins and mods](docs/screenshots/plugins.png) + +### History + +CPU, memory, TPS and player count over time, with an analysis pass that says +what the data means rather than leaving you to read four charts. + +![History](docs/screenshots/history.png) + +### Store and economy + +A server currency, balances, products, crates with published odds, and in-game +delivery that queues while a player is offline. + +![Store](docs/screenshots/store.png) + +### Web panel and public website + +Two more surfaces, each with its own listener, port and access rules. + +![Web panel](docs/screenshots/web-panel.png) + +![Public website](docs/screenshots/site.png) + +--- + ## ✨ Why MSMS? ### Portable by design @@ -441,7 +520,7 @@ These allow current server state and historical activity to be presented separat --- -### 🌉 MSMS Bridge (protocol ready, plugin pending) +### 🌉 MSMS Bridge TPS read over RCON is an approximation, and the console can never report MSPT or player positions at all. The **MSMS Bridge** closes that gap with an in-server plugin that prints marked telemetry lines to the server's **standard output** — a stream MSMS already reads. @@ -451,7 +530,10 @@ The app side is implemented: protocol v1 parsing, and a freshness rule that fall The plugin itself lives in [`bridge/`](bridge/README.md) and builds with a JDK and Node alone — `node bridge/build.mjs` — no Maven or Gradle. Its output is verified against the app's own parser without needing a Minecraft server. -⚠️ It has **not yet been run inside a live server**, so treat the first run as a test. +The app installs it for you from the Players → Live map tab when a server has no +bridge, and it reports through the plugin logger rather than `System.out` so a +Paper server does not nag about it. It has had far less time in live servers +than the desktop core, so treat it as the youngest part of MSMS. 📖 **[Protocol documentation → `docs/bridge-protocol.md`](docs/bridge-protocol.md)** · **[Plugin README → `bridge/README.md`](bridge/README.md)** @@ -526,62 +608,93 @@ npm run dist:portable --- -## 🌐 Web Panel (Beta) +## 🌐 Web Panel -MSMS includes an optional web-management layer for browser/mobile access. +An admin panel in the browser, on its own listener and port. Everything the +desktop app can do to a server, it can do — console, power, players, files, +backups, settings, the store, worlds and the live map — subject to the same +per-server permissions. -Current project design includes: - -- bearer-token authentication -- per-server RBAC concepts -- mobile-friendly access -- dedicated web-panel UI -- separate web server/auth modules +- **Users, roles and per-server scopes.** Nine scopes (`view`, `console`, + `power`, `players`, `files`, `backups`, `settings`, `store`, `worlds`) granted + per server, either directly or through a named role. +- **API keys** for integrations, scoped the same way, with ready-to-paste curl, + JavaScript and Python samples built against your own install's address. A key + can be **disabled** — reversibly, unlike revoking — or deleted from the list. +- **A documented REST surface** at `/api/v1`, with an OpenAPI document and a + human-readable reference the app serves itself. +- **Audit log** of every action, with the actor, the source and the IP. ### Secure defaults -The web panel is: - -- **disabled by default** -- designed to bind to **`127.0.0.1` by default** -- LAN access is an explicit opt-in +- **Disabled by default**, and bound to `127.0.0.1` unless LAN access is + explicitly switched on. +- Cross-origin requests are **denied by default**; browser origins are an + allowlist you fill in. +- Keys are stored as salted SHA-256 and shown exactly once, at creation. ### Important security warning -The built-in web panel does **not** provide HTTPS by itself. +The built-in panel does **not** provide HTTPS by itself. Do not expose it +directly to the public Internet. For remote administration, put it behind a +reverse proxy that terminates TLS, or reach it over a VPN. -Do not expose it directly to the public Internet. +Treat panel tokens and API keys as secrets. -For remote administration, use a properly secured network design with authentication, encryption and access controls appropriate for your environment. +--- -Treat all panel tokens and server credentials as secrets. +## 🛒 Store & Economy ---- +A server currency and shop that players use from the public website. -## 🛒 Store / Economy (Beta) +- **Balances and a ledger** — every change recorded with who made it, why, and + the balance it produced. +- **Products and crates**, with crate odds published to buyers before they buy + and the commands behind a reward never sent to a browser. +- **Stock and per-player limits**, enforced atomically with the balance so a + product cannot oversell. +- **In-game delivery**, queued while a player is offline and released when they + return — a purchase is never silently lost because the server was down. +- **Purchases are audited** like everything else. -MSMS also contains an experimental server store/economy layer. +--- -The implemented project direction includes: +## 🌍 Public Website -- server currency -- player balances -- items/products -- crates -- animated crate-opening presentation -- in-game delivery +A player-facing site on its own listener: news, a storefront, player accounts +linked to Minecraft names, profiles, and a live map. -This part of MSMS is newer than the core server-management features and should be treated as **beta**. +- **Player accounts** verified in game, which works on an offline-mode server + too — a code whispered to the player proves they own the name. +- **Profiles** with per-field publishing controls, so an operator decides what a + visitor may see. +- **Bilingual**, with the site's own language list and default. +- **Mobile-friendly.** --- -## 🌐 Site / CMS Direction +## 🗺️ The Map Page + +A third listener whose entire design is one fullscreen map. -The desktop UI contains a Site section, while the project roadmap has included a broader visual website/CMS direction. +A separate port rather than a path on the website, and that is the point: it +lets you hand the map to people who must not reach the shop or the panel, with +a firewall rule rather than with trust. -The long-term goal is to make server administration and player-facing server services more connected, rather than requiring a completely unrelated tool for every task. +- **Access:** open, a shared passphrase, or signed-in players only. The gate + refuses the *data*, not just the page. +- **Eleven settings** for what it may show — terrain, live positions, names, + skin heads, chunk areas, structures, heatmap, coordinate rounding, and + optionally one pinned world — every one enforced server-side. +- **Named chunk areas.** Mark a region by chunk, give it a colour, a name and a + note; hovering or clicking it shows them. Created by selecting chunks on the + map, by typing coordinates, or over the API. +- **The same map engine** as the panel and the website, so all four surfaces + draw the same world. -Features that are not yet fully implemented should not be considered production-ready until they are completed and documented. +Terrain is read from the server's own region files. MSMS never *generates* +world — a map that could grow a world by being panned would be a map that can +fill a disk. --- @@ -630,8 +743,15 @@ Features that are not yet fully implemented should not be considered production- | Crash analyzer | ✅ | | Update checking | ✅ | | Portable Windows packaging | ✅ | -| Web Panel | 🧪 Beta | -| Store / Economy | 🧪 Beta | +| Web panel (users, roles, scopes) | ✅ | +| REST API + OpenAPI + API keys | ✅ | +| Public website (news, accounts, profiles) | ✅ | +| Store / economy / crates / delivery | ✅ | +| Live map on all surfaces | ✅ | +| Fullscreen map page (own listener) | ✅ | +| Named chunk areas | ✅ | +| Item icons + block colours from the client jar | ✅ | +| MSMS Bridge plugin | ✅ builds, lightly field-tested | | Full visual website/CMS builder | 🗺️ Planned / evolving | --- @@ -1308,7 +1428,9 @@ No. Spigot's BuildTools workflow is intentionally treated differently from direc ### Is the Store/Economy system stable? -It should currently be treated as beta. +Yes — it is covered by the same automated gates as the rest of the app, and +purchases, delivery and balance changes are all audited. The youngest part of +MSMS is the Bridge plugin, not the store. ### Is MSMS affiliated with Mojang or Microsoft? @@ -1360,8 +1482,14 @@ No. - Crash Analyzer - Güncelleme kontrolü - Taşınabilir Windows `.exe` üretimi -- Web Panel **(beta)** -- Store / Economy **(beta)** +- Yönetici **Web Paneli** — kullanıcılar, roller, sunucu bazlı yetkiler +- **REST API**, OpenAPI dokümanı ve kod örnekli **API anahtarları** +- Herkese açık **web sitesi** — haberler, oyuncu hesapları, profiller +- **Mağaza ve ekonomi** — bakiye, ürün, kasa, oyun içi teslimat +- Dört yüzeyde de çalışan **canlı harita** +- Kendi portunda **tam ekran harita sayfası** +- **Adlandırılmış chunk alanları** — renk, ad ve not; arayüzden veya API ile +- Item görselleri ve harita renkleri **Mojang'ın kendi client jar'ından** ### Taşınabilir çalışma mantığı diff --git a/docs/screenshots/create-server.png b/docs/screenshots/create-server.png new file mode 100644 index 0000000..baa9674 Binary files /dev/null and b/docs/screenshots/create-server.png differ diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png new file mode 100644 index 0000000..c666ea1 Binary files /dev/null and b/docs/screenshots/dashboard.png differ diff --git a/docs/screenshots/history.png b/docs/screenshots/history.png new file mode 100644 index 0000000..1878db2 Binary files /dev/null and b/docs/screenshots/history.png differ diff --git a/docs/screenshots/players.png b/docs/screenshots/players.png new file mode 100644 index 0000000..2ecc02f Binary files /dev/null and b/docs/screenshots/players.png differ diff --git a/docs/screenshots/plugins.png b/docs/screenshots/plugins.png new file mode 100644 index 0000000..eaf3284 Binary files /dev/null and b/docs/screenshots/plugins.png differ diff --git a/docs/screenshots/properties.png b/docs/screenshots/properties.png new file mode 100644 index 0000000..9694262 Binary files /dev/null and b/docs/screenshots/properties.png differ diff --git a/docs/screenshots/site.png b/docs/screenshots/site.png new file mode 100644 index 0000000..3ee131f Binary files /dev/null and b/docs/screenshots/site.png differ diff --git a/docs/screenshots/store.png b/docs/screenshots/store.png new file mode 100644 index 0000000..15e7b7f Binary files /dev/null and b/docs/screenshots/store.png differ diff --git a/docs/screenshots/web-panel.png b/docs/screenshots/web-panel.png new file mode 100644 index 0000000..b640c03 Binary files /dev/null and b/docs/screenshots/web-panel.png differ diff --git a/package.json b/package.json index d5eb325..683a09f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msms", - "version": "0.1.0", + "version": "0.2.0", "description": "Minecraft Server Management System — portable, bilingual (EN/TR) desktop control panel for Minecraft servers.", "author": "CaYatur", "license": "MIT", diff --git a/src/main/index.ts b/src/main/index.ts index c797d92..d90ea90 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -29,6 +29,7 @@ import { runBridgeSmoke, runAuditSmoke } from './smoke' +import { runShots } from './shots' import { registerImageScheme, handleImageProtocol, IMG_SCHEME } from './imgProtocol' import { SPLASH_HTML } from './splashHtml' @@ -268,6 +269,17 @@ if (!gotLock) { return } + // Documentation screenshots, taken by the app of itself. Before the splash + // and the main window, because it makes its own. + if (process.env['MSMS_SHOTS']) { + runShots().catch((e) => { + // eslint-disable-next-line no-console + console.log('SHOTS: FAIL - exception', String(e)) + app.exit(1) + }) + return + } + createSplash() initEconomy() initMetrics() diff --git a/src/main/shots.ts b/src/main/shots.ts new file mode 100644 index 0000000..40f2dbf --- /dev/null +++ b/src/main/shots.ts @@ -0,0 +1,212 @@ +import { app, BrowserWindow } from 'electron' +import { join } from 'node:path' +import { mkdirSync, writeFileSync } from 'node:fs' +import * as registry from './core/serverRegistry' +import { updateConfig } from './config' +import { log } from './logger' + +/** + * Documentation screenshots, taken by the app of itself (`MSMS_SHOTS=`). + * + * `webContents.capturePage()` renders the window's own content, so this does not + * photograph the screen: nothing that happens to be in front of the app ends up + * in the README, and the machine's other windows are never touched. The first + * attempt at this used a screen grab and captured a game that was running. + * + * It also runs against a THROWAWAY base directory with servers invented here, so + * the pictures show demo data rather than whoever ran it — a README should not + * publish somebody's real server names, ports or player list. + * + * Reproducible on any machine, which is the point: the screenshots can be + * retaken after a redesign instead of going stale until somebody notices. + */ + +interface Shot { + name: string + /** Runs in the renderer before the capture. Returns when the view is settled. */ + setup: string + /** Extra settle time in ms for views that fetch. */ + wait?: number +} + +/** + * Views are reached by clicking the real controls rather than by poking the + * store: a screenshot taken through a back door can show a state the UI cannot + * actually produce. + * + * By INDEX rather than by label. Matching on text broke twice — the tab bar is + * not rendered at all until a server is selected, and "Web Panel" and "Web Site" + * both contain "web", so both shots came out as the site. + */ +const clickNth = (sel: string, n: number): string => ` + (() => { + const els = [...document.querySelectorAll(${JSON.stringify(sel)})]; + if (!els[${n}]) return false; + els[${n}].click(); + return true; + })()` + +/** Tab order, from `TABS` in App.tsx. */ +const TAB = { + dashboard: 0, console: 1, properties: 2, files: 3, players: 4, plugins: 5, + history: 6, timeline: 7, backups: 8, scheduler: 9, crash: 10, store: 11 +} +/** Sidebar footer order, from Sidebar.tsx: site, web panel, audit, settings. */ +const FOOT = { site: 0, web: 1, audit: 2, settings: 3 } + +const SHOTS: Shot[] = [ + { name: 'dashboard', setup: clickNth('.tab', TAB.dashboard), wait: 1000 }, + { name: 'console', setup: clickNth('.tab', TAB.console), wait: 800 }, + { name: 'properties', setup: clickNth('.tab', TAB.properties), wait: 900 }, + { name: 'files', setup: clickNth('.tab', TAB.files), wait: 1000 }, + { name: 'players', setup: clickNth('.tab', TAB.players), wait: 1200 }, + { name: 'plugins', setup: clickNth('.tab', TAB.plugins), wait: 1000 }, + { name: 'backups', setup: clickNth('.tab', TAB.backups), wait: 800 }, + { name: 'scheduler', setup: clickNth('.tab', TAB.scheduler), wait: 800 }, + { name: 'history', setup: clickNth('.tab', TAB.history), wait: 1400 }, + { name: 'store', setup: clickNth('.tab', TAB.store), wait: 1000 }, + { name: 'web-panel', setup: clickNth('.sidebar-foot .btn', FOOT.web), wait: 1200 }, + { name: 'site', setup: clickNth('.sidebar-foot .btn', FOOT.site), wait: 1200 }, + { name: 'settings', setup: clickNth('.sidebar-foot .btn', FOOT.settings), wait: 800 }, + // Last, because it opens a modal that would otherwise sit over every shot + // after it. + { name: 'create-server', setup: clickNth('.sidebar-actions .btn.primary', 0), wait: 1500 } +] + +/** A couple of servers that plainly are not real, so nobody mistakes them for advice. */ +function seedDemoServers(): void { + const root = join(process.env['MSMS_BASE_DIR'] ?? app.getPath('temp'), 'demo') + const make = (folder: string, name: string, type: string, mc: string): void => { + const path = join(root, folder) + mkdirSync(path, { recursive: true }) + writeFileSync( + join(path, 'server.properties'), + [ + 'motd=A CaYaDev demo server', + 'server-port=25565', + 'max-players=20', + 'online-mode=true', + 'view-distance=10', + 'difficulty=normal', + 'level-name=world' + ].join('\n'), + 'utf-8' + ) + writeFileSync(join(path, 'server.jar'), 'demo', 'utf-8') + mkdirSync(join(path, 'plugins'), { recursive: true }) + const sc = registry.makeServerConfig(path, name) + if (sc) registry.registerServer({ ...sc, type: type as never, mcVersion: mc }) + } + make('survival', 'Survival', 'paper', '1.21.4') + make('creative', 'Creative', 'fabric', '1.21.1') +} + +export async function runShots(): Promise { + const dir = process.env['MSMS_SHOTS']?.trim() + if (!dir) { + // eslint-disable-next-line no-console + console.log('SHOTS: FAIL - MSMS_SHOTS must name an output directory') + app.exit(1) + return + } + mkdirSync(dir, { recursive: true }) + // English, whatever the machine's language is: this README is in English, and + // a screenshot in another one documents nothing for most readers. + updateConfig((c) => { + c.language = 'en' + }) + seedDemoServers() + + const win = new BrowserWindow({ + width: 1400, + height: 900, + show: false, + // Off the desktop's usual area and without a taskbar entry: this window is + // shown only so it repaints, and it should not look like the app opening. + skipTaskbar: true, + backgroundColor: '#0e0f13', + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + contextIsolation: true, + nodeIntegration: false, + // Hidden windows stop painting otherwise, and `capturePage` on a window + // that never painted returns an empty image. + backgroundThrottling: false + } + }) + + if (process.env['ELECTRON_RENDERER_URL']) { + await win.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + await win.loadFile(join(__dirname, '../renderer/index.html')) + } + // Shown, transparent, and never focused. + // + // `capturePage()` on a HIDDEN window returns the last frame it happened to + // paint, which is not the same thing as an empty image — it looks like a + // working screenshot of the wrong screen. Every tab shot came out as the + // console that way, while the page underneath had genuinely switched. A + // window has to be composited to be captured; opacity 0 plus `showInactive` + // gets that without taking focus or covering anything visibly. + win.setOpacity(0) + win.showInactive() + await new Promise((r) => setTimeout(r, 2500)) + + // A server has to be SELECTED before the tab bar exists at all — without this + // every tab shot missed and the run reported 2 of 12. + const picked = await win.webContents.executeJavaScript(clickNth('.server-item', 0)) + if (!picked) log.warn('SHOTS: no server to select; the tab shots will be empty') + await new Promise((r) => setTimeout(r, 1200)) + + // What the selectors actually see, printed once. Guessing at this cost two + // runs: the tab indices were wrong and every shot came out as the console. + const seen = (await win.webContents.executeJavaScript(` + JSON.stringify({ + tabs: [...document.querySelectorAll('.tab')].map((e) => (e.textContent || '').trim()), + foot: [...document.querySelectorAll('.sidebar-foot .btn')].map((e) => (e.textContent || '').trim()) + })`)) as string + // eslint-disable-next-line no-console + console.log('SHOTS: selectors see ' + seen) + let taken = 0 + for (const shot of SHOTS) { + try { + const hit = await win.webContents.executeJavaScript(shot.setup) + if (!hit) { + log.warn(`SHOTS: could not reach "${shot.name}"`) + continue + } + await new Promise((r) => setTimeout(r, shot.wait ?? 700)) + // Clicked again just before the capture. Selecting a server resets the + // view asynchronously, and the first `dashboard.png` came out showing the + // console — a screenshot of the wrong screen, under the right name, which + // is the sort of documentation error nobody spots for a year. + await win.webContents.executeJavaScript(shot.setup) + await new Promise((r) => setTimeout(r, 500)) + const img = await win.webContents.capturePage() + const png = img.toPNG() + if (png.length < 5000) { + log.warn(`SHOTS: "${shot.name}" came back blank (${png.length} bytes)`) + continue + } + writeFileSync(join(dir, shot.name + '.png'), png) + taken++ + // eslint-disable-next-line no-console + console.log(`SHOTS: ${shot.name}.png (${Math.round(png.length / 1024)} KB)`) + // A modal opened for a screenshot has to be closed before the next one, + // or every later shot is taken through it. + await win.webContents.executeJavaScript( + `(() => { document.querySelectorAll('.modal-back, .modal-backdrop').forEach(() => {}); + const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }); + document.dispatchEvent(esc); return true })()` + ) + await new Promise((r) => setTimeout(r, 250)) + } catch (e) { + log.warn(`SHOTS: "${shot.name}" failed:`, e) + } + } + + // eslint-disable-next-line no-console + console.log(`SHOTS: ${taken}/${SHOTS.length} written to ${dir}`) + app.exit(taken > 0 ? 0 : 1) +} diff --git a/src/renderer/src/locales/en.ts b/src/renderer/src/locales/en.ts index 8e1d2ab..2d6d082 100644 --- a/src/renderer/src/locales/en.ts +++ b/src/renderer/src/locales/en.ts @@ -996,6 +996,7 @@ export default { } }, common: { + add: 'Add', save: 'Save', cancel: 'Cancel', browse: 'Browse…', diff --git a/src/renderer/src/locales/tr.ts b/src/renderer/src/locales/tr.ts index bb145c5..ed138f9 100644 --- a/src/renderer/src/locales/tr.ts +++ b/src/renderer/src/locales/tr.ts @@ -1001,6 +1001,7 @@ const tr: typeof en = { } }, common: { + add: 'Ekle', save: 'Kaydet', cancel: 'İptal', browse: 'Gözat…',