Releases: ayozetr/beamng-server-panel
v0.7.0 — in-game map voting + IP-keyed guest identity + upload/parser/config fixes
Follow-up release after v0.5.0, jumping straight to 0.7 at the operator's
request (no 0.6 intermediate). Headlined by in-game chat map voting
end-to-end (PanelBridge v4) and a guest-identity overhaul that keys
returning guests by IP so the same person stops spawning a fresh players
row on every relaunch — with an admin-only manual merge for the residual
duplicates. The rest is a batch of upload / parser / UI correctness fixes
surfaced by real mods on the live server: BeamNG character packs are no
longer badged as cars, vehicle mods carrying client-side Lua are no longer
mistagged as server plugins, and the malformed-info.json tolerance now
also recovers dropped commas between array elements. Rounded out by the
long-broken Server config page finally being editable again, a
warning-free i18n build, the sidebar credit centred, and the voting
feature documented end-to-end.
Added
- In-game map voting end-to-end. Players run
/vote map <name>,
/vote yes//vote no,/vote status,/vote cancelin BeamMP's
chat. PanelBridge v4 handles the state machine (one active vote at a
time, proposer auto-counts as yes, tally re-announced every 15 s)
and on a passing quorum writesvote_result.json. The panel reads
it, broadcasts a countdown viacommands.json, then runs the same
_doActivateMap(..., {autoRestart:true})primitive the Maps page
uses for manual activation. Plugin also broadcasts a periodic
"tip: start a vote with /vote map" everysuggestIntervalSecwhen
no vote is in flight, listing the maps the operator allowed.
Configurable from the Maps page (Votación de mapa en juegocard):
enabled toggle, duration, quorum %, min votes, countdown, suggestion
interval, optional allowed-maps whitelist. Dashboard shows a live
tally + countdown when a vote is open; admins get a Cancel button.
New endpointsGET /api/maps/vote,PUT /api/maps/vote/config
(admin),POST /api/maps/vote/cancel(admin). Voter identity uses
the same stable-id scheme as players (bmp:<uid>/guest:<name>/
…) so a guest can't double-vote and a player reconnecting on a
different slot still maps to the same ballot. Full wire protocol +
defaults documented indocs/live-state.mdanddocs/api.md. /vote mapwith no name now lists the available maps. Running
/vote mapwith no argument (or a name not on the allowed list) used
to reply with a bare usage/error line, leaving the player to guess
the exact name. Both replies now append "Available maps: "
from a singleallowedMapNames()helper (shared with the periodic
tease); when the operator hasn't restricted the list, the line says
any level is allowed instead of showing an empty list.- Guest players keyed by IP, with an admin-only manual merge. With
BeamMP forum registration disabled everyone connects as a guest, and
_stableIdForwas keying guests on BeamMP'sguestXXXXXXXhandle —
which re-rolls on every game relaunch, so one person spawned a fresh
players row each session (16 live rows were really 4 people / 4 IPs).
identifiers.ipnow wins right after the forum UID, so a returning
guest collapses back onto one row (thebmp:branch still takes over
automatically if registration ever returns). For the residual drift
(two people behind one NAT, or an ISP rotating a dynamic IP) a new
POST /api/players/merge(admin-only, irreversible, audit-logged)
re-pointsplayer_events/player_nicknames/bansonto a chosen
survivor, widens itsfirst_seen/last_seen, drops the source rows
and republishes bans — all in one transaction. The Players history
table gains an admin-only "Fusionar" toggle with per-row checkboxes,
a keep-which selector and a ConfirmModal.
Fixed
- Vehicle mods with client-side Lua no longer mistagged as server
plugins. The upload classifier flagged anymain.luaat any
depth as a BeamMP server plugin, which sent vehicle mods that ship
a client-side script undervehicles/<X>/lua/main.lua
(turbo / traction-control / scripted gimmicks — the Nissan 180
RPS13 mod was the trigger) into the server-plugin extraction
branch. That branch enforces a "single root folder" rule, so the
upload was rejected withServer plugin archive must contain exactly one root folder. Found: art, ui, vehicles. Classifier now
ignoresmain.luafiles that live under thevehicles/or
levels/subtrees, so client-side car / track scripts no longer
derail the upload — the archive falls into the client-mod branch
and is copied verbatim toResources/Client/<name>.zipas
intended. - BeamNG character packs are classified as their own kind, not
"Coche". A character pack shipsvehicles/<x>/info.jsonbecause the
engine models the walking avatar as a vehicle, so the Mods page
badged it "Coche" and the Cars catalogue would have listed it as a
drivable car._classifyClientModnow reads each
vehicles/<x>/info.json'sType:"Characters"→ new'character'
kind, anything else stays'car'(stops at the first real car, so
multi-car packs cost one read).enumerateCustomCarsskips
Type=="Characters"the same way, so the Mods badge and Cars
catalogue stay aligned. Frontend gains a "Personajes"/"Characters"
filter chip, badge label and anIconUserspalette. - The Server config page is editable again. Changing any field
(MaxCars, Tags, Name…) reverted instantly and the Save bar never
appeared, so saving was unreachable — which is why the live
ServerConfig.tomlstill held all-default values. In
useServerConfigthe initial-load effect depended onreload, a
useCallbackover[toast, t]wheretis a fresh
window.AppI18n.t.bind()every render, soreload's identity
changed each render, re-firing the effect →reload()→setDirty({})
and wiping the operator's edit (plus an unbounded/api/config
refetch loop). The effect now depends only oncanEdit, so config
loads once (and on permission flips) and dirty state survives until
Save. - Malformed-
info.jsonrecovery now handles dropped commas between
array elements, not just between property keys.2K-Happogahara
ships aninfo.jsonwith two missing commas — one between
"features"and"suitablefor"(the original Sadamine fix) and a
second between two adjacent{}objects inside thespawnPoints
array — so it parsed half the file then died at position 509, meta
came back null and the catalogue card stayed on the SVG. The regex
now matches any value-end token (string /}/]/true/
false/null/ number) followed by whitespace+newline and a
value-start token, so Happogahara parses to its 3 spawn points and
picks up itsquickrace/TH.jpgthumbnail. Sadamine still parses
identically and Nordschleife (trailing-comma-only) is unchanged;
parser-level fix, no frontend changes. - Removed 6 duplicate i18n keys (
profile.copied,config.lang,
config.lang_sub,pl.col.last,common.delete,common.refresh)
that were each defined twice inen+es, triggering 12 esbuild
duplicate-key warnings and silently dropping the earlier definition.
Build is now warning-free; the only user-visible change is es
common.refresh, which now resolves to the intended "Actualizar"
instead of the stray "Refrescar". - Sidebar credit centred. The "Desarrollado por ayozetr" link in
the sidebar footer is now horizontally centred (justifyContent: centeron the flex anchor).
Docs
- Voting documented end-to-end.
docs/live-state.mdgains the
vote_config.json/vote_state.json/vote_result.jsonwire
formats + a player-side chat-command table;docs/api.mddocuments
the three/api/maps/vote*endpoints; the PanelBridge README bumps
to v4 with an in-game voting commands table for operators. - ROADMAP Phase 1 ticked. The AC purge was long done but never
checked off — verified each named symbol/file/regex returns zero
hits in the tree, marked the section "(done)" and added
forward-pointers (apiCars / SteamIDs / LiveMapCard) to the phases
where their BeamMP replacements landed. Phase 8 also picked up the
voting + classifier bullets.
v0.5.0 — content quality of life + ops + perms + audit-driven hardening
Big batch release rolling up the entire ops-quality-of-life pass driven by
the external audit + several follow-up feature drops the operator asked for
during a single working session: granular permissions (mapActivate so a
non-admin user can change maps without inheriting full ServerConfig.toml
rights), one-click BeamMP-Server binary update from the dashboard banner,
a Cars catalogue mirroring Mapas, custom-map metadata + thumbnails read
from info.json (including info.json files BeamMP-Server's own parser
tolerates but JSON.parse rejects), CPU temperature on the Dashboard,
four new KPI tiles (mods-on-disk excluding Kunos vanilla, joins / laps /
total drivers), strictly admin-only mod deletion + ConfirmModal, the
Mods catalogue collapsed by default, a stable per-player id that doesn't
collapse two guests behind the same NAT into one entry, and the active-
map tile in the Dashboard now shows modder title + screenshot for custom
maps too. Plus the security / health / audit-export hardening from the
external audit and a fast unit-test layer in front of the existing smoke
run. Two coordinated panels — BeamMP + Assetto Corsa siblings — got the
shared Dashboard tiles (mods-on-disk, joins / laps / drivers, CPU temp)
identically, with the AC panel filtering Kunos vanilla out of the disk
total.
Changed
- Mod deletion is now strictly admin-only.
DELETE /api/mods/<loc>/<name>
switched from themodUploadpermission gate tocheckAdminAuth.
modUploadstill controls who can push new archives in, but the
destructive side moves up to admin — the wrong delete on a busy
server (operator picks the wrong row, modder's hash mismatches
mid-rotation, …) kicks every connected player and forces a manual
reupload, and the panel's risk model treats that as a single-actor
operation. Frontend mirrors the change: the Mods catalogue's delete
button only renders when the viewer is admin (was: when they had
upload rights), and the existing ConfirmModal continues to gate
the click. Same approach as the assetto-server-panel sibling, where
car / track delete has always required admin. - Mod classifier no longer tags vehicle-extension mods as "Coche".
The previous heuristic flagged any zip containingvehicles/<X>/<Y>.jbeam
as a car — which caught full-vehicle mods but also license-plate packs,
body kits, bumpers and trim that ship plate / tuning.jbeamfiles
for stock vehicles without aninfo.jsonof their own. EuroPlates_JorgePinto.zip
was the prompting case: Mods page said "Coche", Cars catalogue
(which requiresvehicles/<X>/info.json) said "no car" — inconsistent.
Strict criterion now:kind=cariff at least onevehicles/<X>/info.json
exists. Vehicle-extension mods classify asother.
Added
- Collapsible "Installed mods" card on the Mods page. Catalogue
is informational; the uploader below is the action surface. Card
starts collapsed; click the header to expand. Total-mods badge stays
visible even when collapsed. Preference persisted in
localStorage['bp-mods-installed-open']. Same idiom the Dashboard
activity card already uses (chevron indicator, keyboard handler,
aria-expanded). - Stable per-player id (no more "everyone is UUID 0"). PanelBridge
consumer was using BeamMP-Server's per-session slot number (0, 1,
2…) as the canonical player_id, collapsing every guest that ever
held slot 0 into a single DB row. New_stableIdFor(p)returns
bmp:<uid>for registered BeamMP users (forum UID — durable across
sessions / IPs / name changes),guest:<name>for guests (BeamMP
assigns each guest a session-unique handle like "guest8099544" so
two guests behind the same NAT no longer collapse into one panel
entry — the operator pointed this out as the genuine common case
during BeamMP-auth maintenance windows when AllowGuests has to stay
true), andnamed:<name>/slot:<n>as deeper fallbacks.
_connectedPlayersis rekeyed by stableId; each entry carries
slotIdso MP.DropPlayer still works for kicks. Migration #13
purges legacy slot-numeric rows from players / player_events / bans.
apiPlayerKick + apiPlayerBan resolve the stableId → slotId via the
live map. _publishBans writes each ban under both keys (prefixed +
raw) so the in-tree PanelBridge keeps matching without a reload. - Player nickname rendered in Dashboard players-online list.
Matches the Players page's "Nickname (in-game)" shape so the
admin-set nickname is visible from both views at a glance. Falls
back to the in-game name when no nickname is set. Single inline
ternary; no new fetch. - Active-map Dashboard tile now shows modder title + thumbnail for
custom maps. Vanilla maps were already rendering bundled
screenshots — but the moment the operator switched to a Nürburgring
mod the tile fell back to the raw directory name + the technical
/levels/ks_nord/info.jsonpath, with no image. apiDashboardExtra
now resolves custom maps via enumerateCustomMaps and surfaces
{ source, levelName, hasPreview }. Frontend builds the preview
URL from/api/maps/preview/<source>/<levelName>— same endpoint
the Maps page card uses, so the active-map thumbnail and the
catalogue thumbnail stay in sync. mapActivategranular permission. The User role can now be
granted access to the Maps + Cars pages and to changing the active
map without inheriting full ServerConfig.toml editing rights. Closes
the gap that "let a user pick the next map" required handing over
the whole config editor (port, max players, AuthKey rotation
scope, etc).ROLE_PERMISSIONSgrows amapActivateentry which
is checked byapiMaps,apiMapsActivate,apiMapPreview,
apiCarsandapiCarPreviewinstead ofserverConfig.
Map rotation (apiMapsRotation+apiMapsRotationUpdate) keeps
theserverConfiggate — the rotation timer rewrites the live
config and restarts the server, so it's a config-level operation
not an operator-level one. New_applyImplicationshelper makes
serverConfigautomatically grantmapActivateat runtime so an
existing admin or any user already trusted with serverConfig keeps
Maps + Cars access without the operator having to tick a second
checkbox. UI: a new row in Users > Permissions explains the
difference; the sidebar entries for Maps + Cars now hide via
requires:'mapActivate'; the Map rotation card on the Maps page
is only rendered when the viewer has full serverConfig. New i18n
keysperm.mapActivate+perm.mapActivate_hint(en + es).- CPU temperature badge on the Dashboard's "Uso CPU" tile. Small
number in the top-right corner of the existing CPU card, no layout
changes. Sourced from/sys/class/thermal/thermal_zone*/temp— pure
kernel surface, no extra package, no sudo, plug-and-play on every
modern Linux host._getCpuTempCwalks each zone, prefers ones whose
typematches a CPU sensor (x86_pkg_temp,coretemp,
cpu[-_]thermal,core_N,package_N) and reports the highest
reading among the preferred set so a multi-package box shows the
hottest die. On hosts where the thermal tree doesn't exist (some VPS,
LXC containers, FreeBSD, Windows) or every reading is out of plausible
range, the helper returns null and the badge simply doesn't render —
the tile looks exactly like before. - One-click BeamMP-Server binary update from the dashboard banner.
POST /api/server/update-binary(gated onserverControl) downloads
the asset matching the host's distro/arch from the latest GitHub
release, makes a<bin>.bakof the running binary, atomically
replaces it (after stopping the service), restarts, verifies
--versionon the new binary, and rolls back to the.bakif the
new one is corrupted. Process-wide flag rejects concurrent calls with
409._detectLinuxAssetmaps/etc/os-release+process.archto
BeamMP'sBeamMP-Server.<id>.<version_id>.<arch>shape (Debian
12/13, Ubuntu 22.04/24.04, x86_64 + arm64); other distros refuse
with a 400 rather than ship a libc-mismatched binary. The Dashboard
update banner grows an "Actualizar" button next to "Ver release"
with a ConfirmModal that spells out the disruption. - Cars catalogue page. New
Cochesentry in the sidebar's
Contenido group, mirroring the Mapas page UX but read-only — BeamMP
auto-streams car mods to every connecting client, so there is no
activate flow.enumerateCustomCarswalksResources/Client/*.zip
pairingvehicles/<X>/info.jsonwithvehicles/<X>/default.(png|jpg),
emits one card per vehicle with brand / country / year / type
badges, and surfaces authors + description in the modal. Vanilla
BeamNG.drive cars are deliberately NOT enumerated — they ship with
each player's local game install — and the page leads with an
explanatory card making that explicit. - Custom maps now read their
levels/<X>/info.jsonso the
catalogue shows the modder's title, description, authors, country
(last segment of thelevels.common.country.*translation key),
biome and features rather than the bare directory name. Per-level
pairing means a track pack that ships multiple levels in one .zip
gets one card with the correct thumbnail per level._parseLooseJson
tolerates the JSONC dialect BeamNG uses (trailing commas, occasional
comments) so files like the Nordschleife mod's info.json don't
reject under strictJSON.parse. - Per-level preview extractor.
/api/maps/preview/<source>/<level>
extracts the targetedlevels/<level>/preview.(png|jpg|jpeg)from
inside the zip — separate from the mods page's first-preview-found
endpoint because a multi-level archive needs one thumbnail per
level, not one for the whole archive. - Ban enforcement at join (PanelBridge v3). The plugin gains an
onPlayerAuthhandler that readsResources/Server/PanelBridge/bans.json
on every auth attempt and refuses the connectio...
v0.2.0 — first feature release
First feature release of the BeamMP fork. v0.1.x got the panel functionally equivalent to the AC original on the BeamMP side; v0.2.0 is the wave of operator-facing convenience features built on top of that base — visual catalogues for maps and mods, automated lifecycle (map rotation, restart scheduler, MOTD loop), live moderation surface (chat viewer, broadcast templates), and dashboard polish (active-map screenshot card, BeamMP-Server update notifier, BeamMP-specific KPI widgets). One CSRF fix and a second documented deploy layout round it out.
Added
- Visual map catalogue at
/maps— grid of cards with the official BeamNG.drive screenshots (fromdocumentation.beamng.com/official_content/levels/images/, normalised to 800×450 q82), click-to-modal, activate-with-restart-prompt. TheMapfield in Server Config is now a read-only display + "Choose in Maps" button. - Visual mods catalogue at
/mods— unified.car-gridof thumbnail cards, kind-based filter chips, click-to-modal, per-card preview image extracted on demand from inside the.zip. - Live chat viewer on the Players page +
PanelBridgev2 Lua plugin. Plugin records everyonChatMessageinto a 200-line ring-bufferedchat.json; panel polls every 5 s with?since=<ts>for incremental refresh. - Broadcast button + templates modal in Players + MOTD loop in Settings. Five starter templates and an optional re-broadcast every N minutes for server rules / Discord links. Reuses
/api/broadcast+ PanelBridge command pipeline. - Automatic map rotation in Maps — persisted list + interval, in-process timer activates the next map every N minutes and restarts BeamMP-Server.
- Cron-simple restart scheduler in Settings — fixed time + weekday picker, persisted in
panel_settings. Audit-logged each scheduled restart. - BeamMP-Server update notifier banner on the Dashboard — queries GitHub releases once per 6 h, compares against local
BeamMP-Server --version, shows a per-version-dismissable banner when a newer server is out. - BeamMP-specific KPI widgets on the Dashboard — second row with active map (rendered as a screenshot card background under a darkening gradient overlay), joins in last 24 h, mods on disk, binary version. Polled at
/api/dashboard/extraevery 30 s. - ServerConfig.toml export / import in Settings — one-click download (AuthKey redacted) and upload through the existing
_validateTomlUpdatespipeline. Cross-host migration + disaster-recovery workflow with the same per-key validation + audit logging. - Quick map switcher dropdown in the topbar — current map name + popover with vanilla and custom maps, click-to-activate with confirm-restart. Gated on
serverConfigpermission.
Fixed
checkOriginhonoursX-Forwarded-Hostwhen sitting behind a trusted reverse proxy (TRUST_PROXY=1). The original implementation compared againstreq.headers.hostalone, which rejected every cookie-authenticatedPOST/PUT/DELETE/PATCHwhen the upstream proxy rewrote Host (Caddy defaultreverse_proxy, nginx withoutproxy_set_header Host, cloudflared withhttpHostHeader: localhost). Every same-origin rejection now logs peer + headers.
Docs
docs/install-beammp-split-layout.md+docs/install-panel.md— alternative deploy layout validated end-to-end by a real prod deploy on a shared host that already runsassetto-server-panel. The originalinstall-ubuntu.md(dedicatedbeammpuser under/srv/beammp/) stays as the greenfield default..env.exampleTRUST_PROXYblock expanded to describe the new CSRF /X-Forwarded-Hostgate.maps.attributioni18n string (EN + ES) rewritten now that thumbnails are real screenshots.
Polish
- Service worker
CACHE_NAMEbumpedbp-panel-v4→bp-panel-v5so PWA installs drop the v4 bundle on next visit. - Broadcast button relocated from the player-list card header into the live-chat card header.
Full changelog: CHANGELOG.md · Diff: v0.1.1...v0.2.0
v0.1.1 — port AC v1.6.0→v1.7.1 game-neutral fixes
Game-neutral fixes ported from upstream assetto-server-panel v1.6.0→v1.7.1. 13 of the 15 upstream commits in that range were AC saved-session-presets work that doesn't apply to BeamMP and were skipped (see CHANGELOG.md for the explicit list with reasoning).
Fixed
-
dist/auto-rebuild whennode server.jsis invoked directly — cherry-picked fromassetto-dashboard@c18f40d. The package.jsonprestarthook that regeneratesdist/fromsrc/only fires undernpm start. The systemd unit indocs/install-ubuntu.mdrunsnode server.jsdirectly, sogit pull && systemctl restart beamng-panelwas serving a stale UI bundle until someone rannode build.jsby hand. Adds anensureBuildFresh()IIFE at the top ofserver.jsthat walkssrc/, takes the max mtime, compares againstdist/app.js's mtime, and runsbuild.jsviaspawnSynconly when stale or missing. Warm restarts pay one stat per source file and zero subprocess. Build failures log a warning but don't block the boot. -
7zabinary +x bit self-heal — cherry-picked fromassetto-dashboard@c1d9c9f.npm install --ignore-scripts, tarball restores andnode_modulescopies between hosts all silently drop the executable bit onnode_modules/7zip-bin/linux/x64/7za, which made.7zserver-plugin uploads fail with HTTP 500 from a downstreamspawn EACCES. Adds an idempotentfs.chmodSync(sevenBin.path7za, 0o755)right after therequire('7zip-bin')so every panel boot self-heals the bit.
Skipped from upstream
13 commits in the AC v1.6.0..v1.7.1 window are part of the saved-session-presets feature (the session_presets table, the Plantillas/Presets sidebar entry, the inline builder modal, the presetManage permission, the export/import wire format, and four CSS / hook-count / icon-padding fixes against that same modal stack). Session+Preset doesn't map to BeamMP's free-roam multiplayer model, so the whole feature was left behind during the Phase 0/1 AC purge and stays out of scope for this fork.
Polish
- Service worker
CACHE_NAMEbumpedbp-panel-v3→bp-panel-v4.
v0.1.0 — first proper release of the BeamMP fork
First proper release of the BeamMP fork. Closes the seven-phase migration from assetto-server-panel v1.6.0 — see ROADMAP.md for the per-phase audit and CHANGELOG.md for the full release notes.
What shipped
- Server lifecycle — Start / Stop / Restart against a real
BeamMP-Serverbinary with detach + log-FD survive-restart trick + adopt-by-pgrep + TCP-port health-check. Audit + rate-limit + global mutex. Auto-restart on config save. ServerConfig.tomleditor — Line-edit parser that preserves comments, key order, inline whitespace. 17-field validated PUT with atomic write + timestamped backups.AuthKeyredacted in GET, write-only in PUT, never logged in audit.- Mods catalogue — Listing of
Resources/Client/*.zip+Resources/Server/<plugin>/folders with per-mod size + file count +hasMainLuaflag + delete-with-confirm. Upload classifier matches BeamMP's spec (client mods stay as.zipverbatim, server plugins extract into a folder). - Live players + moderation — Optional PanelBridge Lua plugin streams the player list into the panel via filesystem; the panel writes kick/broadcast commands back. Bans persist in SQLite. Join/leave history.
- Carry-over from the AC fork — Multi-user accounts with role-based granular permissions, TOTP 2FA, audit-log hash chain with retention sweeper + boundary rows, scrypt + constant-time compare, HttpOnly+SameSite+Secure cookies, CSRF Origin check, CSP without unsafe-eval/inline, rate-limited login + change-password + server-control + mod-upload + per-user SSE cap, forced password change for seeded admin, refusal to delete the last admin, dark/light themes, EN/ES UI, Docker-ready.
Docs
docs/install-ubuntu.md— install BeamMP-Server on Ubuntu 22.04 / 24.04 (dedicated user, AuthKey, systemd unit, UFW, panel wiring, updates, troubleshooting).docs/server-lifecycle.md— how the panel manages the BeamMP-Server process.docs/server-config.md—/api/configreference (field schema, AuthKey handling, comment-preservation, backups, atomic writes, audit, auto-restart).docs/live-state.md— live player state via the PanelBridge Lua plugin.docs/authentication.md— auth + 2FA + role permissions.
Known follow-ups (not in v0.1.0)
- PanelBridge v2 plugin: enforce bans at
onPlayerAuth, push chat events back to the panel's Logs page, optional Discord webhook per join/leave/ban. - Surface live player count in the topbar widget (the
/api/metricsplayersfield still reads0). - "Restore from backup" UI for
ServerConfig.toml(the.bakfiles exist on disk; just needs a list + restore endpoint pair). - Retention sweeper for
player_events.
Status
The fork was end-to-end smoke-tested against fake fixtures but has NOT been deployed against a real BeamMP-Server in production yet — the 0.1.0 number is intentional. v1.0 would follow after at least one production deploy + one external user exercising the moderation surfaces.