HZ Scripts

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.

Available since HZ-Bridge v1.1.0. Older versions only shipped the framework / inventory bridge.

Opening the panel

In-game (chat, F8 console, or keybind):

/hzpanel
  • Console-only (server F8) — /hzpanel from the server console does not open the NUI. Type it from in-game.
  • Requires the ACE permission configured under HZBridge.Panel.AcePermission (default admin) — see Permissions setup below.
  • Press Escape or click the close button to leave. Sessions are token-gated and time out after HZBridge.Panel.SessionTokenTtlMs (5 min default) — re-open to refresh.
You can also open straight to a specific module's tab from another script:
-- 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.

qb-core boilerplate gotcha: a fresh qb-core 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_ace lines to copy-paste, with your license already interpolated
Look at the red error in your console — the fix is right there. Available since v1.1.1.
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:

ModuleWhat you can tune
HZ-BridgeGlobal theme — accent, surfaces, gradient mode, radius scale
HZ-WeatherZones (editor + map), forecast, time, seasons, events, config
HZ-TelevisionChannels, defaults, performance toggles
HZ-AudioMixerPer-category volume defaults, per-player profile overrides
Third-party scripts can add their own tab simply by shipping a 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

TypeRenders asNotes
booleanToggle switch
numberNumeric input with min / max / stepSlider variant when step ≤ 1 and range is bounded
stringText input, optional pattern + maxLength
enumDropdown (single choice)Choices via choices = { 'a', 'b', 'c' }
enum-multiConstrained multi-select (toggle chips)New in v1.1.0 — stored as list
colorHex colour pickerValidated as #RRGGBB
keybindKeybind capture (single key + modifiers)
listTag input (free-form add/remove)
listInline table editor with itemSchema columns
vehicle-listVehicle picker gridAuto-populated from GetAllVehicleModels()
weapon-listWeapon picker gridStatic catalog, shipped under web/src/data/weapons.ts
actionButton that fires a server eventUse for "Apply preset" / "Reset" style triggers
enum-actionDropdown of actions
viewFull-width custom React componentRegistry under web/src/views//

Sections, groups, tabs

  • section(id, { label, tab, dependsOn }, body) — visual grouping inside a module tab. tab is the sub-tab id (declared via module(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. opts supports default, description, requiresRestart, deprecated, hidden, dependsOn, plus the type-specific extras.
  • invariant(sectionId, fn) — cross-field validation. Runs on every save attempt. Return nil (ok) or a string (error message).
  • migrations({ [N] = function(values) ... end }) — bump schemaVersion and 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

AxisKeysEffect
Accentaccent / accentHi / goldVice signature trio (pink / hot pink / gold). Presets: Vice, Industrial.
Surfacessurface / subpanel / tintOuter panel / inner sub-panel / hover tint. RGB only — opacity decided per consumer. Presets: Vice surfaces, Neutral surfaces.
Gradient modegradientMode = 'gradient' \'solid'solid collapses every accent gradient to a flat fill — calmer / office-tool feel.
RadiiradiusNone / Xs / Sm / Md / Lg / XlSix-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).

A full copy-paste reference (Lua forwarder + TypeScript + vanilla JS applier + CSS defaults + Tailwind binding + hide-with-HUD pattern) ships inside the 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 buggy bridge_schema.lua can fail to load (logged) but can't crash the server.
  • Rate-limited writes — default 10 writes per 5s per source. Configurable via HZBridge.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 / max checks, then section-level invariant functions 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 than HZBridge.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/.json via tmpfile + rename. Pending writes use data/.pending.json and are committed atomically on successful save.
  • 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