v1.14.0
Changes since v1.13.6
Summary
This update requires you to add this to your server.cfg
add_unsafe_child_process_permission luxu_adminThis release adds offline player inventory management, introduces a new child-process–based filesystem for server tooling, and overhauls the quick-search experience with new operators (off:, nearby:, job:, grade:). It also reworks the moderation UI, replaces sprite polling with reactive state-bag handlers, and fixes several permission and refresh bugs.
What's New
Features
- Offline player inventory management — edit an offline player's inventory directly from the admin panel. Includes two new overridable queries (
getOfflinePlayerInventory/setOfflinePlayerInventory) inconfig/database/queries.jsfor server owners using custom inventory scripts. - Child-process based filesystem — server-side filesystem operations now run inside a dedicated child process for better isolation and stability.
- Smarter quick search — new search operators make finding players much faster:
off:searches offline playersnearby:<radius>finds players within a radius of youjob:<name>filters by jobjob:<name> grade:<grade>filters by job grade (number or name)
- Refreshed moderation UI — cleaner layout and improved flows on the Moderation page.
- Reactive sprite system — player sprites are now driven by state-bag handlers instead of polling, reducing client overhead.
Bug Fixes
- Fixed permissions for viewing IP and identifiers.
- Fixed gamertag refresh after player data changes.
- Improved sprite update logic for reliability.
Other
- Rewrote the in-panel Help content for Getting Started and Quick Search sections to reflect the new search behaviour.
File Changes
⚙️ Configuration & Bridge Files
These files are not encrypted. Review the changes below — you may need to manually apply updates to your server's config files.
fxmanifest.lua
diff --git a/resource/luxu_admin/fxmanifest.lua b/resource/luxu_admin/fxmanifest.lua
index 70e32fe..e208929 100644
--- a/resource/luxu_admin/fxmanifest.lua
+++ b/resource/luxu_admin/fxmanifest.lua
@@ -8,7 +8,7 @@ use_experimental_fxv2_oal 'yes'
author 'luxu-gg'
description 'Admin menu for FiveM'
-version '1.13.6'
+version '1.14.0'
dependencies {
'oxmysql',config/config.json
diff --git a/resource/luxu_admin/config/config.json b/resource/luxu_admin/config/config.json
index a646fba..873b611 100644
--- a/resource/luxu_admin/config/config.json
+++ b/resource/luxu_admin/config/config.json
@@ -3,7 +3,7 @@
"allow_second_fivem_instance": true,
"owners": ["license2:d020a5e4212b8f58fe054a92393dde19c9576361", "license2:85276b4347e328371328427b3b125b38fdacb011"],
"hierarchy_protection": true,
- "default_duty": true,
+ "default_duty": false,
"default_dark_mode": true,
"branding": {
"name": "FiveM Community",config/database/queries.d.ts
diff --git a/resource/luxu_admin/config/database/queries.d.ts b/resource/luxu_admin/config/database/queries.d.ts
index 087d2ea..e252f1d 100644
--- a/resource/luxu_admin/config/database/queries.d.ts
+++ b/resource/luxu_admin/config/database/queries.d.ts
@@ -1,113 +1,127 @@
import type { OfflinePlayer } from '../../../../shared/types/database';
declare const queries: {
- // -------------------------------------------------------------------------
- // Player
- // -------------------------------------------------------------------------
-
- /** Fetches a single player row by their charId. */
- getPlayer(charId: string): Promise<any>;
-
- /**
- * Returns raw retention timestamps for every player in the database.
- * ESX reads from `users`; QB/QBX reads from `luxu_stats_player_retention`.
- */
- getPlayerRetention(): Promise<{ timestamp: number }[]>;
-
- /**
- * Retrieves the saved clothing/skin data for a player.
- * ESX reads `skin` from `users`; QB/QBX reads from `playerskins`.
- */
- getPlayerClothing(charId: string): Promise<Record<string, any>>;
-
- // -------------------------------------------------------------------------
- // Vehicles
- // -------------------------------------------------------------------------
-
- /** Returns all vehicles of a given model hash with their owner's charId. */
- getVehicleOwners(modelHash: number): Promise<{ plate: string; char_id: string }[]>;
-
- /**
- * Returns the vehicle mod/properties data for a given plate.
- * ESX reads `vehicle` from `owned_vehicles`; QB/QBX reads `mods` from `player_vehicles`.
- */
- getVehicleMods(plate: string): Promise<Record<string, any> | null>;
-
- // -------------------------------------------------------------------------
- // Offline player operations
- // -------------------------------------------------------------------------
-
- /**
- * Fetches a full offline player profile by their charId.
- * Returns `null` when no matching record is found.
- */
- getOfflinePlayer(charId: string): Promise<OfflinePlayer | null>;
-
- /**
- * Searches offline players by a search term (minimum 2 characters).
- * Override this to customise the search behaviour for your database schema.
- */
- searchOfflinePlayers(term: string): Promise<{ name: string; charId: string; identifiers: string[] }[]>;
-
- /**
- * Permanently deletes a single player row from the framework table.
- * For a full character deletion with cleanup transaction, use `deleteCharacter`.
- */
- deleteOfflinePlayer(charId: string): Promise<boolean>;
-
- /**
- * Deletes a player character inside a single database transaction.
- * Add extra cleanup queries (vehicles, housing, etc.) directly in this method in queries.js.
- * Each query receives the character identifier as its only parameter.
- */
- deleteCharacter(charId: string): Promise<boolean>;
-
- /** Changes the first/last name of an offline player. */
- changeOfflinePlayerName(charId: string, newName: string): Promise<boolean>;
-
- /** Changes the job and grade of an offline player. */
- changeOfflinePlayerJob(charId: string, job: string, grade: number): Promise<boolean>;
-
- /** Changes the gang and grade of an offline player. */
- changeOfflinePlayerGang(charId: string, gang: string, grade: number): Promise<boolean>;
-
- /** Replaces all account balances for an offline player. */
- changeOfflinePlayerMoney(charId: string, accounts: Record<string, number>): Promise<boolean>;
-
- /**
- * Sets the balance of a single account for an offline player.
- * Reads the current accounts JSON, patches the target key, and writes it back.
- */
- updateOfflinePlayerAccountMoney(charId: string, account: string, amount: number): Promise<boolean>;
-
- // -------------------------------------------------------------------------
- // Inventory
- // -------------------------------------------------------------------------
-
- /**
- * Finds all players that possess a specific item.
- * Uses MariaDB `JSON_CONTAINS` for server-side filtering.
- */
- getPlayersWithItem(itemName: string): Promise<Array<{ charId: string; name: string; amount: number }>>;
-
- // -------------------------------------------------------------------------
- // Statistics (framework tables only)
- // -------------------------------------------------------------------------
-
- /** Returns the top 100 players ranked by total money (bank + cash). */
- getRichList(): Promise<Array<{ name: string; money: number; charId: string }>>;
-
- /** Returns a job-name → player-count mapping from the framework player table. */
- getJobDistribution(): Promise<Record<string, number>>;
-
- /** Returns the total number of distinct players (by license) ever registered. */
- getTotalUniquePlayers(): Promise<number>;
-
- /** Returns the total number of character rows (one per character/slot). */
- getTotalCharacters(): Promise<number>;
-
- /** Returns the total bank balance across all players. */
- getTotalBankMoney(): Promise<number>;
+ // -------------------------------------------------------------------------
+ // Player
+ // -------------------------------------------------------------------------
+
+ /** Fetches a single player row by their charId. */
+ getPlayer(charId: string): Promise<any>;
+
+ /**
+ * Returns raw retention timestamps for every player in the database.
+ * ESX reads from `users`; QB/QBX reads from `luxu_stats_player_retention`.
+ */
+ getPlayerRetention(): Promise<{ timestamp: number }[]>;
+
+ /**
+ * Retrieves the saved clothing/skin data for a player.
+ * ESX reads `skin` from `users`; QB/QBX reads from `playerskins`.
+ */
+ getPlayerClothing(charId: string): Promise<Record<string, any>>;
+
+ // -------------------------------------------------------------------------
+ // Vehicles
+ // -------------------------------------------------------------------------
+
+ /** Returns all vehicles of a given model hash with their owner's charId. */
+ getVehicleOwners(modelHash: number): Promise<{ plate: string; char_id: string }[]>;
+
+ /**
+ * Returns the vehicle mod/properties data for a given plate.
+ * ESX reads `vehicle` from `owned_vehicles`; QB/QBX reads `mods` from `player_vehicles`.
+ */
+ getVehicleMods(plate: string): Promise<Record<string, any> | null>;
+
+ // -------------------------------------------------------------------------
+ // Offline player operations
+ // -------------------------------------------------------------------------
+
+ /**
+ * Fetches a full offline player profile by their charId.
+ * Returns `null` when no matching record is found.
+ */
+ getOfflinePlayer(charId: string): Promise<OfflinePlayer | null>;
+
+ /**
+ * Searches offline players by a search term (minimum 2 characters).
+ * Override this to customise the search behaviour for your database schema.
+ */
+ searchOfflinePlayers(term: string): Promise<{ name: string; charId: string; identifiers: string[] }[]>;
+
+ /**
+ * Permanently deletes a single player row from the framework table.
+ * For a full character deletion with cleanup transaction, use `deleteCharacter`.
+ */
+ deleteOfflinePlayer(charId: string): Promise<boolean>;
+
+ /**
+ * Deletes a player character inside a single database transaction.
+ * Add extra cleanup queries (vehicles, housing, etc.) directly in this method in queries.js.
+ * Each query receives the character identifier as its only parameter.
+ */
+ deleteCharacter(charId: string): Promise<boolean>;
+
+ /** Changes the first/last name of an offline player. */
+ changeOfflinePlayerName(charId: string, newName: string): Promise<boolean>;
+
+ /** Changes the job and grade of an offline player. */
+ changeOfflinePlayerJob(charId: string, job: string, grade: number): Promise<boolean>;
+
+ /** Changes the gang and grade of an offline player. */
+ changeOfflinePlayerGang(charId: string, gang: string, grade: number): Promise<boolean>;
+
+ /** Replaces all account balances for an offline player. */
+ changeOfflinePlayerMoney(charId: string, accounts: Record<string, number>): Promise<boolean>;
+
+ /**
+ * Sets the balance of a single account for an offline player.
+ * Reads the current accounts JSON, patches the target key, and writes it back.
+ */
+ updateOfflinePlayerAccountMoney(charId: string, account: string, amount: number): Promise<boolean>;
+
+ // -------------------------------------------------------------------------
+ // Inventory
+ // -------------------------------------------------------------------------
+
+ /**
+ * Reads the raw inventory JSON array for an offline player.
+ * Returns `null` when the player row doesn't exist, or `[]` when the
+ * column is empty/malformed.
+ */
+ getOfflinePlayerInventory(charId: string): Promise<any[] | null>;
+
+ /**
+ * Writes the full inventory JSON array for an offline player.
+ * The caller is responsible for producing items in a shape the framework
+ * (or inventory script) can read back.
+ */
+ setOfflinePlayerInventory(charId: string, items: any[]): Promise<boolean>;
+
+ /**
+ * Finds all players that possess a specific item.
+ * Uses MariaDB `JSON_CONTAINS` for server-side filtering.
+ */
+ getPlayersWithItem(itemName: string): Promise<Array<{ charId: string; name: string; amount: number }>>;
+
+ // -------------------------------------------------------------------------
+ // Statistics (framework tables only)
+ // -------------------------------------------------------------------------
+
+ /** Returns the top 100 players ranked by total money (bank + cash). */
+ getRichList(): Promise<Array<{ name: string; money: number; charId: string }>>;
+
+ /** Returns a job-name → player-count mapping from the framework player table. */
+ getJobDistribution(): Promise<Record<string, number>>;
+
+ /** Returns the total number of distinct players (by license) ever registered. */
+ getTotalUniquePlayers(): Promise<number>;
+
+ /** Returns the total number of character rows (one per character/slot). */
+ getTotalCharacters(): Promise<number>;
+
+ /** Returns the total bank balance across all players. */
+ getTotalBankMoney(): Promise<number>;
};
export default queries;config/database/queries.js
diff --git a/resource/luxu_admin/config/database/queries.js b/resource/luxu_admin/config/database/queries.js
index 379d8eb..af5c32d 100644
--- a/resource/luxu_admin/config/database/queries.js
+++ b/resource/luxu_admin/config/database/queries.js
@@ -520,6 +520,71 @@ module.exports = {
// Inventory
// ---------------------------------------------------------------------------
+ /**
+ * Reads the raw inventory JSON array for an offline player.
+ *
+ * Returns the parsed array straight from the framework's player row
+ * (`users.inventory` for ESX, `players.inventory` for QB/QBX). Returns `null`
+ * when the row doesn't exist, or `[]` when it exists but the column is empty
+ * or malformed.
+ *
+ * Item shapes vary between frameworks and inventory scripts:
+ * - ESX-native / qb-inventory: `{ name, count | amount, slot?, info?, metadata? }`
+ * - ox_inventory: `{ name, count, slot, metadata? }`
+ * The TS layer normalises them before handing them to the UI.
+ *
+ * Override this method if your server stores items in a separate table
+ * (e.g. `qs_inventory`, `codem_inventory`) rather than the framework column.
+ *
+ * @param {string} charId
+ * @returns {Promise<any[] | null>}
+ */
+ async getOfflinePlayerInventory(charId) {
+ if (Framework.name === 'esx') {
+ const row = await oxmysql.single('SELECT inventory FROM users WHERE identifier = ?', [charId]);
+ if (!row) return null;
+ try {
+ const parsed = typeof row.inventory === 'string' ? JSON.parse(row.inventory) : row.inventory;
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ } else if (Framework.name === 'qb' || Framework.name === 'qbx') {
+ const row = await oxmysql.single('SELECT inventory FROM players WHERE citizenid = ?', [charId]);
+ if (!row) return null;
+ try {
+ const parsed = typeof row.inventory === 'string' ? JSON.parse(row.inventory) : row.inventory;
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Writes the full inventory JSON array for an offline player.
+ *
+ * The caller is responsible for producing the item shape. `offline_actions.ts`
+ * dual-writes both `amount`/`count` and `metadata`/`info` so that any
+ * framework or inventory script reader will pick up the correct field.
+ *
+ * Override this method if your server stores items in a separate table.
+ *
+ * @param {string} charId
+ * @param {any[]} items
+ * @returns {Promise<boolean>}
+ */
+ async setOfflinePlayerInventory(charId, items) {
+ const json = JSON.stringify(Array.isArray(items) ? items : []);
+ if (Framework.name === 'esx') {
+ return (await oxmysql.update('UPDATE users SET inventory = ? WHERE identifier = ?', [json, charId])) === 1;
+ } else if (Framework.name === 'qb' || Framework.name === 'qbx') {
+ return (await oxmysql.update('UPDATE players SET inventory = ? WHERE citizenid = ?', [json, charId])) === 1;
+ }
+ return false;
+ },
+
/**
* Finds all players that possess a specific item.
* Uses MariaDB `JSON_CONTAINS` for server-side filtering — significantly fasterconfig/help.lua
diff --git a/resource/luxu_admin/config/help.lua b/resource/luxu_admin/config/help.lua
index f8db609..3832b25 100644
--- a/resource/luxu_admin/config/help.lua
+++ b/resource/luxu_admin/config/help.lua
@@ -8,17 +8,16 @@ return { {
["title"] = "Getting Started",
["description"] = "Learn how to use the panel",
["content"] = [[
-The luxu_admin panel is a web application that allows you to manage your server. It is designed to be used by server owners and staff members.
+The `luxu_admin` panel is your web-based control center for server moderation, player management, and staff tooling.
-The panel is designed to be used by server owners and staff members. It is a web application that allows you to manage your server.
+Use it to:
-The player will be kicked from the server and their character will be deleted
+- View connected players and open their management page.
+- Search online players instantly with <kbd>Ctrl + K</kbd>.
+- Manage offline players with the `off:` search operator.
+- Review staff tools, punishments, notes, and other server utilities.
-This will equip the outfit for either male or female staff members
-
-This will update the current group's outfit to match your current outfit and ped model
-
-This will update the current group's outfit to match your current outfit and ped model
+Tip: most actions are available directly from the player management view after selecting a player from the search modal or player list.
]]
},
@@ -26,41 +25,39 @@ This will update the current group's outfit to match your current outfit and ped
["title"] = "Quick Search",
["description"] = "Searching players is very easy",
["content"] = [[
-<kbd>Ctrl + K</kbd> Press to open the search bar
-
-<p class="font-semibold">Examples:</p>
-
-- id:1 will search for a player with the server id 1.
-- license:4551 will search for a player with the license identifier 4551.
-- ip:127.0.0.1 will search for a player with the ip 127.0.0.1.
-- name:John will search for a player with the name John.
-- steam:123 will search for a player with the steam id 123.
- ]]
- },
+Press <kbd>Ctrl + K</kbd> to open the search bar.
+You can search by free text or by using operators.
- {
+<p class="font-semibold">Supported operators:</p>
- ["title"] = "Staff Clothing",
- ["description"] = "How to create and use staff clothing",
- ["content"] = [[
-Staff clothing is activated by toggling the duty switch, located in the top right corner of the this panel.
+- `off:` Search offline players.
+- `nearby:` Search players near you within a radius.
+- `job:` Filter players by job name or label.
+- `grade:` Combine with `job:` to filter by job grade number or grade name.
-To create clothing for the staff groups, do the following:
+<p class="font-semibold">Examples:</p>
-- Open your clothing menu
-- Equip your character how you would like it
-- Open the panel and go to the staff menu
-- Open the edit staff group menu
-- Click on the Clone Yourself button
-- Select the gender you would like to assign the clothing to
-- Click on the save button
-- Repeat the process for the other gender
-- Now once you are on duty, you will have the clothing on
- ]]
+- `1` finds the player with server id `1`.
+- `john` searches online players by name and other indexed fields.
+- `off:john` searches offline players.
+- `nearby:50` finds players within a radius of `50`.
+- `job:police` finds players with the police job.
+- `job:police grade:officer` finds police players with the `officer` grade.
+- `job:ems grade:4` finds EMS players with grade `4`.
+
+<p class="font-semibold">Sample queries:</p>
+
+```text
+off:john
+nearby:50
+job:police
+job:police grade:officer
+job:ems grade:4
+```
+
+Use the arrow keys to move through the results and press <kbd>Enter</kbd> to open the highlighted player.
+ ]]
},
-
-
-
}Full list of files changed
| Change | File |
|---|---|
| Modified | client/cl_main.lua |
| Modified | client/modules/actions.lua |
| Modified | client/modules/entity_inspector.lua |
| Modified | client/modules/nui.lua |
| Modified | client/modules/sprites.lua |
| Modified | config/config.json |
| Modified | config/database/queries.d.ts |
| Modified | config/database/queries.js |
| Modified | config/help.lua |
| Modified | fxmanifest.lua |
| Modified | server/modules/jail.lua |
| Modified | web/ |