Admin Panel
HZ-Bridge ships a centralised admin panel that every HZ-Script module plugs into. Instead of each script shipping its own command + UI for config, everything lives behind a single /hzpanel accessible to admins. The panel auto-builds a typed editor for every config field declared in each module's bridge_schema.lua.
Opening the panel
In-game (chat, F8 console, or keybind):
/hzpanel
- Console-only (server F8) —
/hzpanelfrom the server console does not open the NUI. Type it from in-game. - Requires the ACE permission configured under
HZBridge.Panel.AcePermission(defaultadmin) — see Permissions setup below. - Press
Escapeor click the close button to leave. Sessions are token-gated and time out afterHZBridge.Panel.SessionTokenTtlMs(5 min default) — re-open to refresh.
-- server export — opens the panel for src and preselects the named module
exports['HZ-Bridge']:OpenPanel(src, 'HZ-Weather')
Permissions setup
The panel is gated by the canonical FXServer admin ACE node. Two server.cfg lines are required:
add_principal identifier.license:YOUR_LICENSE group.admin
add_ace group.admin admin allow
Replace YOUR_LICENSE with the player license from your server (visible by typing /hzpanel once — the failed-check console output prints all your identifiers + a copy-paste-ready block with your license already interpolated).
You can paste both lines directly into the server console for an instant fix — no restart required. If you put them in server.cfg, restart FXServer (or re-exec server.cfg) so they're loaded.
server.cfg ships with add_ace group.admin command allow but not add_ace group.admin admin allow. qb-core's own admin menu uses its DB-based qbcore_admins table rather than the canonical admin ACE node, so the line is absent. Almost every HZ-Bridge permission ticket comes from this — add the second line and you're done.The framework probe falls back to qb-core / qbx_core / ESX / ox_core admin tables if the ACE check fails. So a player marked as admin in your qbcore_admins table can open the panel even without the ACE setup. But ACE is the recommended path because it's framework-agnostic and auditable in one place.
If the check fails, the server console prints :
- The exact ACE node being checked
- All your player identifiers
- An ACE probe matrix (which nodes are granted, which aren't)
- The two exact
add_principal+add_acelines to copy-paste, with your license already interpolated
command.hzpanel does NOT grant the panel. A common mistake is to try add_ace identifier.X command.hzpanel allow. That node is unused by HZ-Bridge — the check is on the configured AcePermission (default admin), not on the command name.Custom ACE node
If you don't want to use the canonical admin node (e.g. you only want a specific subset of staff to open the panel without giving them every other admin command), configure a custom node in config.lua :
HZBridge.Panel.AcePermission = 'hzbridge.panel.admin'
Then in server.cfg :
add_principal identifier.license:YOUR_LICENSE group.hzbridge.staff
add_ace group.hzbridge.staff hzbridge.panel.admin allow
The fallback (when AcePermission is nil or '') is 'admin' — aligned on the config default since v1.1.1. Older versions used 'HZ-Bridge.admin' as the fallback, which was a documented mismatch and locked out customers who only had the canonical admin ACE granted. Upgrade to v1.1.1 if you're affected.
What you can edit
The panel auto-discovers every resource that ships a bridge_schema.lua file at its root. As of HZ-Bridge v1.1.0, the following modules ship a schema and integrate into the panel:
| Module | What you can tune |
|---|---|
| HZ-Bridge | Global theme — accent, surfaces, gradient mode, radius scale |
| HZ-Weather | Zones (editor + map), forecast, time, seasons, events, config |
| HZ-Television | Channels, defaults, performance toggles |
| HZ-AudioMixer | Per-category volume defaults, per-player profile overrides |
bridge_schema.lua — see the schema DSL section below.
The schema DSL
Each module's bridge_schema.lua is a small Lua DSL that declares the config the panel exposes. The DSL runs in a sandboxed environment (no _G, no raw io, restricted string / table / math subsets) so a buggy or malicious schema can't break the server.
Minimal example
-- resources/your-script/bridge_schema.lua
module('your-script', {
schemaVersion = 1,
label = { en = 'Your Script', fr = 'Ton script' },
}, function()
section('general', {
label = { en = 'General', fr = 'Général' },
}, function()
field('Enabled', 'boolean', {
default = true,
label = { en = 'Enabled', fr = 'Activé' },
description = {
en = 'Master on/off switch for the script.',
fr = 'Interrupteur global du script.',
},
})
field('PlayerLimit', 'number', {
default = 32, min = 1, max = 256, step = 1,
label = { en = 'Max concurrent players', fr = 'Joueurs simultanés max' },
})
end)
end)
That's it — open /hzpanel, the "Your Script" tab appears with two typed fields, value changes are written back to disk and broadcast to every client that subscribes.
Field types
| Type | Renders as | Notes |
|---|---|---|
boolean | Toggle switch | |
number | Numeric input with min / max / step | Slider variant when step ≤ 1 and range is bounded |
string | Text input, optional pattern + maxLength | |
enum | Dropdown (single choice) | Choices via choices = { 'a', 'b', 'c' } |
enum-multi | Constrained multi-select (toggle chips) | New in v1.1.0 — stored as list |
color | Hex colour picker | Validated as #RRGGBB |
keybind | Keybind capture (single key + modifiers) | |
list | Tag input (free-form add/remove) | |
list | Inline table editor with itemSchema columns | |
vehicle-list | Vehicle picker grid | Auto-populated from GetAllVehicleModels() |
weapon-list | Weapon picker grid | Static catalog, shipped under web/src/data/weapons.ts |
action | Button that fires a server event | Use for "Apply preset" / "Reset" style triggers |
enum-action | Dropdown of actions | |
view | Full-width custom React component | Registry under web/src/views/ |
Sections, groups, tabs
section(id, { label, tab, dependsOn }, body)— visual grouping inside a module tab.tabis the sub-tab id (declared viamodule(name, { tabs = { ... } })).dependsOn = { field = 'X', equals = true }collapses the section when the parent condition is false.group(id, { label }, body)— inline collapsible group inside a section.field(id, type, opts)— a single field.optssupportsdefault,description,requiresRestart,deprecated,hidden,dependsOn, plus the type-specific extras.invariant(sectionId, fn)— cross-field validation. Runs on every save attempt. Returnnil(ok) or a string (error message).migrations({ [N] = function(values) ... end })— bumpschemaVersionand ship a migration to rewrite old saved values forward.
Custom views
If you need a UI more bespoke than the typed-form renderer can produce (a map editor, a graph, a calendar...), declare a view field and register the React component:
-- bridge_schema.lua
field('ZonesEditor', 'view', {
component = 'HZ-Weather:ZoneEditor',
events = {
upsertZone = 'hz_weather:admin:upsertZone',
deleteZone = 'hz_weather:admin:deleteZone',
},
})
// web/src/views/HZ-Weather/index.tsx
import { registerView } from '../registry'
import { ZoneEditor } from './ZoneEditor'
registerView('HZ-Weather:ZoneEditor', ZoneEditor)
HZ-Weather ships six views as reference implementations: Map, ZoneEditor, Events, Forecast, SeasonsPanel, TimePanel.
Theme cascade (Apparence)
The Apparence tab in HZ-Bridge centralises the visual tokens every HZ NUI shares. A single change retints HZ-Weather, HZ-AudioMixer, HZ-Television live — no restarts. New scripts plugging in inherit the cascade automatically as long as their NUI reads the canonical --hz-* CSS variables.
Axes you control
| Axis | Keys | Effect | |
|---|---|---|---|
| Accent | accent / accentHi / gold | Vice signature trio (pink / hot pink / gold). Presets: Vice, Industrial. | |
| Surfaces | surface / subpanel / tint | Outer panel / inner sub-panel / hover tint. RGB only — opacity decided per consumer. Presets: Vice surfaces, Neutral surfaces. | |
| Gradient mode | gradientMode = 'gradient' \ | 'solid' | solid collapses every accent gradient to a flat fill — calmer / office-tool feel. |
| Radii | radiusNone / Xs / Sm / Md / Lg / Xl | Six-tier corner scale. Presets: Sharp (0/2/4/6/8/10), Soft (0/3/6/8/12/16), Round (0/4/8/12/16/20), Industrial (0/0/2/2/4/4). |
How modules consume it
The full payload is broadcast every time the admin saves:
-- server (any resource)
AddEventHandler('HZ-Bridge:theme:updated', function(theme)
-- theme: { preset, accent, accentHi, gold, surface, subpanel, tint,
-- gradientMode, radiusNone, radiusXs, radiusSm, ..., radiusXl }
end)
-- client
RegisterNetEvent('HZ-Bridge:theme:updated', function(theme)
SendNUIMessage({ action = 'theme', data = theme })
end)
In your NUI, write each key as a CSS variable on :root (e.g. --hz-accent-rgb, --hz-surface-rgb, --hz-radius-md). Components reference the variables via rgb(var(--hz-accent-rgb) / 0.45) / var(--hz-radius-md) so the cascade is automatic.
Get the current theme synchronously on boot:
local theme = exports['HZ-Bridge']:GetTheme()
Or request it via TriggerServerEvent('HZ-Bridge:theme:request') — the server replies with TriggerClientEvent('HZ-Bridge:theme:updated', src, theme).
hz-fivem-ui Claude skill at examples/theme-cascade-boilerplate.md.Safety + audit
- Sandboxed schemas — DSL runs without raw
io,os,debug, and with a restricted standard library. A buggybridge_schema.luacan fail to load (logged) but can't crash the server. - Rate-limited writes — default
10 writes per 5sper source. Configurable viaHZBridge.Panel.RateLimitWrites/RateLimitWindowMs. - Session tokens — every panel open issues a short-lived token; expired tokens get a fresh bootstrap on next request.
- Validation pipeline — every save goes through field type validation, optional
pattern/min/maxchecks, then section-levelinvariantfunctions before the value lands on disk. - Audit log — every accepted save writes a line to the rolling audit file (
data/audit-YYYY-MM-DD.log). Rotated files older thanHZBridge.Panel.AuditRetentionDays(default 90) are pruned at boot. Set to 365 if you need RGPD retention. - Atomic writes — the effective config per module is written to
data/via tmpfile + rename. Pending writes use.json data/and are committed atomically on successful save..pending.json - Per-source ACE check on every action — not just at panel open. A revoked admin can't keep editing on a still-open panel.
Reference
- Schema DSL source:
modules/panel/schema_dsl.lua - Server API:
modules/panel/api_server.lua - Theme module:
modules/panel/theme.lua - React panel:
web/src/App.tsx - Effective + pending config:
data/(atomic) +.json data/(in-flight).pending.json - Audit:
data/audit-YYYY-MM-DD.log
