Browser based editor for BlueMenu menus, served at menu.blueva.net.
This is the web frontend behind /bm editor in BlueMenu. Server owners edit their Java chest menus and their Bedrock forms from the browser instead of hand writing YAML, and the plugin applies the result on the live server.
The plugin is the source of truth. Menu files stay under plugins/BlueMenu/menus/, and the editor talks to the server over a WebSocket connection, so nothing is copied into a separate account or database that could drift from what the server actually loads.
- A player runs
/bm editorin game. The plugin asks the editor to open a session and hands the player a link. - Opening the link registers that browser window and gives it its own verification id, so a leaked link does not grant access on its own.
- If
require-confirmationis on insettings.yml, the plugin waits for/bm confirm <id>in game before the session becomes editable. - From then on the browser edits over plain HTTP. Every operation that needs the server is forwarded to the plugin and answered by it.
Sessions expire on their own, and the editor rejects any request whose session is unknown, consumed or unconfirmed.
The plugin is not a client the editor can call directly, so requests travel to it over a broadcast channel and come back over HTTP:
- The server registers once and stores its credentials in
webeditor-credentials.yml. - It subscribes to its own private Reverb channel and keeps itself marked as reachable with a heartbeat.
- A browser request that needs the server publishes an RPC request on that channel and waits; the plugin performs the operation and posts the answer back to
/api/plugin/rpc-response.
If Reverb is unreachable or unconfigured, the plugin says so on its heartbeat and collects the same requests from /api/plugin/rpc-poll once a second instead. The editor keeps working either way; the channel only makes it immediate.
That wait happens inside the browser's request while the answer arrives on a different request, so the web process must serve more than one request at a time. With a single worker the waiting request starves the one that would release it:
PHP_CLI_SERVER_WORKERS=6 php artisan serve --host=0.0.0.0 --port=8000 --no-reloadartisan serve only honours PHP_CLI_SERVER_WORKERS together with --no-reload.
The plugin is handed these values at runtime, so they must be set on the server rather than at build time:
REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET
REVERB_HOST=menu.example.net # the public hostname, not localhost
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_HOST is the host the plugin and the browser connect to, so localhost only ever works for local development. Leave the values empty and both fall back to polling, which works but adds up to a second of latency to every operation.
Start the server with php artisan reverb:start and give it a route from the public host. Behind Cloudflare, WebSockets must be enabled for the zone.
Reverb needs two paths proxied to it, not one:
| Path | Used by | Direction |
|---|---|---|
/app/{key} |
browser and plugin | the WebSocket itself |
/apps/{id}/events |
this application | publishing an event |
Routing only /app leaves the sockets working while every publish fails. Nothing breaks visibly, because a failed announcement is logged rather than raised, but the other open windows stop hearing about changes.
Opening the site without a session shows the editor running on the example menus the plugin ships. Everything renders and every editor works; only saving is unavailable, because there is no server behind it.
- Laravel 13 on PHP 8.3+, with Laravel Reverb for the channel the plugin listens on
- React 19 with TypeScript 7, mounted from a Blade shell
- CodeMirror 6 for the YAML editor,
yamlfor the document model - Vite 8 with the Laravel plugin, Tailwind 4 and Bunny Fonts
- SQLite by default, MySQL in production
- PHPUnit for the backend, Vitest for the editor
app/
├── Enums/ domain vocabularies
├── Events/ broadcast events, including the plugin RPC request
├── Http/Controllers/Api/ browser endpoints
├── Http/Controllers/Api/Plugin/ endpoints the plugin calls
├── Models/ servers, sessions, verifications, settings
└── Services/ session lifecycle, RPC bridge, maintenance, admin console
resources/
├── demo/ the sample menus the plugin ships
├── js/editor/ menu model, yaml codec, validator, materials
├── js/components/ editor chassis
├── js/components/visual/ canvas, item editor, form builder, animator
└── views/ Blade shells that mount React
routes/web.php browser routes
routes/plugin.php plugin routes
routes/channels.php private channel authorisation
public/editor/items/ item sprites, one folder per Minecraft version
| Component | Version |
|---|---|
| PHP | 8.3+ |
| Composer | 2+ |
| Node | 20+ |
| Database | SQLite for local work, MySQL 8+ in production |
| Reverb | Started with php artisan reverb:start |
composer setupThat single command installs dependencies, creates .env, generates the app key, migrates and builds the assets. To run it by hand:
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
npm install
npm run buildThen start the dev server:
composer devcomposer dev runs php artisan dev, which brings up the PHP server, the queue worker, the log tailer and Vite together. If a frontend change does not show up, the Vite process is probably not running.
npm run dev # Vite dev server with hot reload
npm run typecheck # tsc --noEmit
npm test # Vitest, covering the yaml codec and the editor logic
npm run build # production assets
composer test # clears config, then runs PHPUnit
php artisan reverb:startRun the narrowest test set that covers a change, for example php artisan test --compact --filter=SomeTest.
.github/workflows/deploy.yml typechecks, builds and runs both test suites on every push and pull request. On main it then pulls, installs production dependencies, rebuilds the assets, migrates and warms the caches on the host.
- Blueva
- Whiron
- Arthuurrr
Website: blueva.net
Licensed under the GNU General Public License v3.0.
