Skip to content

feat(adminmenu): add a modular, hardened admin menu - #117

Closed
ASTROWwwW wants to merge 18 commits into
esx-framework:mainfrom
ASTROWwwW:main
Closed

feat(adminmenu): add a modular, hardened admin menu#117
ASTROWwwW wants to merge 18 commits into
esx-framework:mainfrom
ASTROWwwW:main

Conversation

@ASTROWwwW

@ASTROWwwW ASTROWwwW commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

Adds esx_adminmenu, building on @zoxile's #111. The original menu is the first
commit; the rest refactor it for modularity, harden it, and add features. Each
commit is scoped to one concern for step-by-step review.

Changes

  • Table-driven dispatch instead of the if/elseif chains, one permission gate per namespace
  • Method probing via function references, not type(x) == "function" (fixes giveMoney, takeMoney, cleanInventory)
  • Multi-identifier bans, matched on connect; backward compatible with license-only rows
  • Admin action log: batched writes, redacted, 30-day retention, logAdminAction / getAdminLogs exports, optional webhook. Replaces the unused kicks table
  • Unified online/offline player search + an Admin Logs tab

Tested locally (automated suite + in-game), compatible with es_extended 1.14.

PR Checklist

  • My commit messages and PR title follow the Conventional Commits standard.
  • My changes have been tested locally and function as expected.
  • My PR does not introduce any breaking changes.
  • I have provided a clear explanation of what my PR does, including the reasoning behind the changes and any relevant context.

zoxile and others added 17 commits July 21, 2026 02:24
Adds AdminLimits caps (props bytes, page sizes, plate attempts, offline search) and AdminMenu tuning (model timeout, blip, spectate, troll, waypoint, server-data interval) consumed by the server and client refactors.
Introduces safeQuery/safeScalar/safeUpdate, a registerCallback wrapper that always answers exactly once, and a getActionPermission accessor. Routes read paths through the safe wrappers, self-heals the impounds cache, derives translations from the locale table, and replaces recursive plate generation with a loop.
getActiveBans and BanCache.get no longer mutate the cache on read; a shared prune runs on expiry change and a maintenance timer. initDB steps are isolated so ban loading always runs, and the playerConnecting ban check is wrapped so a lookup failure resolves the deferral instead of hanging the join.
PlayerAction and ServerManagement become permission-gated handler registries with a single config-sourced gate; all server callbacks go through the registerCallback wrapper; xPlayer lookups use ESX.GetPlayerFromId (portable on stock ESX); and the helper copy-commands share one factory.
Adds onResourceStop, spectate-failure and respawn state restoration; collapses the NUI server bridges, quick toggles and vehicle cycles behind factories; dedupes trim and model loading; and makes client_scripts an explicit ordered list (main.lua renamed to init.lua).
README gains a FeaturePermissions catalog for operators building granular groups; en.lua gains server-facing error keys.
Bans now capture every enabled identifier (license, discord, steam, ... per Config.Ban.Identifiers) and the ban cache indexes each, so a banned player is matched on reconnect via ANY identifier instead of license only. Adds an identifiers column with migration and a connect check over all of the player's identifiers; legacy license-only bans still match. Also caches the sorted active-ban view (rebuilt only on cache change via a generation counter) so pagination no longer re-sorts every request, and anchors the vehicle search to a prefix so its index can be used.
Replaces the SUBSTRING_INDEX filter in the offline player search with an indexable IN (license, license2) lookup so it no longer forces a full users table scan.
ESX is obtained through exports["es_extended"]:getSharedObject(), so xPlayer
methods cross a resource boundary and arrive as function references: tables
carrying a __call metamethod. type(x) == "function" is therefore always false
for them, and the guards using it rejected every call.

giveMoney, takeMoney and cleanInventory could never succeed, reporting that the
ESX build did not expose a method it did expose.

Helpers.isCallable accepts a function or a callable table. It reimplements what
ESX.IsFunctionReference does so the resource keeps working on builds predating
that helper.
The uptime was formatted as %02d:%02d:%02d over days, hours and minutes, which
reads as a HH:MM:SS clock. Eight minutes of uptime displayed as 00:00:08 and
looked like eight seconds standing still.

Units are now explicit: 42s, 8m, 2h 15m, 3d 04h.
The offline search only accepted a complete license: or charN: identifier, and
its results were dropped anyway: the callback never returned a success field,
which the UI checks before rendering. Searching had therefore never displayed
anything.

Searching now matches partial identifiers, first name, last name, full name and
phone number, and merges connected players into the same result set, deduplicated
by identifier. Group, phone number and first-seen date are returned as well.

A complete identifier still takes the indexed path, so the common precise lookup
does not degrade into a table scan. LIKE metacharacters are escaped, otherwise a
single % would return every user in one request.
Records who did what to whom, in a new admin_logs table. Only actions are
recorded, never reads: the menu polls player and vehicle lists constantly and
would otherwise drown the log. Permission denials are recorded too, since a
blocked attempt is the more interesting signal.

Entries are queued in memory and written in batches by a background thread, so
an admin action never waits on the database. The queue is bounded and sheds its
oldest entries rather than growing without limit, every write goes through the
safe query helper, and a flush on resource stop keeps the last window.

Payloads are size-capped, and a payload replaced by a truncation marker stays
valid JSON. Network identity and credential-shaped keys are dropped before
storage. Records are rate-limited per source: server callbacks are reachable by
any client, so denials could otherwise be spammed until real entries were
evicted.

Retention defaults to 30 days, purged hourly in bounded chunks so the table is
never locked for long. An optional Discord webhook accepts https only and
forwards an allowlist of actions.

Replaces the kicks table, which was created and written to but never read back
by anything. Any existing table is left in place: a resource dropping tables on
start is a footgun.

Adds logAdminAction and getAdminLogs exports, the resource having exposed none
until now, and a logViewer permission separate from admin.
Recent Players was fed only by playerDropped, so it listed players who had
disconnected since the resource started and stayed empty the rest of the time.

Any successful action on a connected player now adds them straight away. The
entry keeps their server id and an online status so the UI can act on it live;
playerDropped later rewrites the same identifier as offline, and deduplication
by identifier prevents a duplicate.

The snapshot is taken after the handler runs, so an entry reflects the state the
action produced rather than the one preceding it.
An offline ban stores only the license, which looks like an oversight next to
the online path capturing every identifier. It is deliberate: the player is not
connected, so their discord and steam identifiers cannot be read, and ESX does
not persist them.
A new sidebar tab renders the log as an activity feed rather than a table. Each
action has its own sentence built from its payload, so a row reads "Astrxw gave
$5.000 to Tester1 (bank)" instead of showing raw JSON. A colour marker and chip
carry the category, timestamps are relative, and a details button reveals the
exact time, identifiers and every payload field.

Search runs on the server across admin, target and action, so it reaches the
whole retained history rather than only the loaded page. It is debounced, needs
two characters, and escapes LIKE metacharacters. Category and period filters sit
alongside it, and the scroller pulls the next page in as it nears the bottom.

Rows expand on demand, so a virtual list would mis-measure them: a plain
overflow scroller is used instead. The bundle is rebuilt from these sources; the
tab folder is admin-logs rather than logs because the Vite template gitignores a
bare logs path, which would have dropped the sources from the repository.
@CLAassistant

CLAassistant commented Jul 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

The init payload no longer carries the translation table. The client reads the active locale it already ships and the NUI merges it over its English defaults, so a partial locale keeps every key.
@N0tNvll
N0tNvll self-requested a review August 4, 2026 23:33
@N0tNvll N0tNvll added the enhancement New feature or request label Aug 4, 2026
@N0tNvll

N0tNvll commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Some parts needs to be fixed i will merge and fix by myself

@N0tNvll N0tNvll closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants