diff --git a/README.md b/README.md
index b15c095..08f216f 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,9 @@
Features •
Server Software •
Getting Started •
- Web Panel •
+ Screenshots •
+ Web Panel •
+ Map Page •
Development •
Roadmap
@@ -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.
+
+
+
+### 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.
+
+
+
+### `server.properties`
+
+Every setting with an explanation, grouped and searchable, plus raw editing for
+anything MSMS does not know about.
+
+
+
+### Players and the live map
+
+Moderation, world controls, the NBT inventory viewer, and a live map rendered
+from the server's own region files.
+
+
+
+### Plugins and mods
+
+Installed jars with enable/disable, plus Modrinth search and one-click install.
+
+
+
+### 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.
+
+
+
+### Store and economy
+
+A server currency, balances, products, crates with published odds, and in-game
+delivery that queues while a player is offline.
+
+
+
+### Web panel and public website
+
+Two more surfaces, each with its own listener, port and access rules.
+
+
+
+
+
+---
+
## ✨ 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…',