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_admin

This 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) in config/database/queries.js for 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 players
    • nearby:<radius> finds players within a radius of you
    • job:<name> filters by job
    • job:<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 faster

config/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/