v1.14.9

Summary

Skipped v1.14.8. This release improves inventory snapshots and report/chat tooling, and expands bridge/config options for reports, logs.

What's New

Features

  • Entity opacity control for props / entity inspector
  • Markdown support in chat and report messages
  • Vehicle database plate search (exact match) and search-type tabs
  • Improved jaksam_inventory stash/trunk/glovebox item listing
  • Report feature toggles: video_record, voice_channel, transcripts (plus new transcript permissions)
  • logs.discord_only option to skip panel log storage and use Discord only
  • Snapshot lookup columns/indexes for better death-inventory performance
  • Clothing menu reliability and custom clothing builder improvements

Bug Fixes

  • Vehicles plate search no longer leaves the loading spinner stuck
  • Ace permissions correctly show for the owner group; NUI callback error fixed
  • Custom clothing builder variants fixed
  • Staff member create flow fixed
  • Prop delete reliability fixes
  • Blips fixes
  • Snapshot query/cleanup performance improvements

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.

bridge/server/inventory.lua

             return items
         end
     elseif isResourceRunning("jaksam_inventory") then
+        local invId = id
         if typeofStash == "trunk" or typeofStash == "glovebox" then
-            return exports['jaksam_inventory']:getInventoryIdFromPlate(id, typeofStash)
-        else
-            return exports['jaksam_inventory']:getInventory(id)
+            invId = exports['jaksam_inventory']:getInventoryIdFromPlate(id, typeofStash)
+            if not invId then return {} end
+        end
+
+        local jaksamInventory = exports['jaksam_inventory']:getInventory(invId)
+        if not jaksamInventory or type(jaksamInventory.items) ~= "table" then return {} end
+
+        local items = {}
+        for slotKey, item in pairs(jaksamInventory.items) do
+            if type(item) == "table" and item.name then
+                items[#items + 1] = {
+                    name = item.name,
+                    amount = item.amount,
+                    slot = tonumber(item.slot) or tonumber(tostring(slotKey):match("%d+")),
+                    metadata = item.metadata,
+                }
+            end
         end
+
+        return items
     elseif isResourceRunning("core_inventory") then
         return exports.core_inventory:getInventory("stash-" .. id)
     end

bridge/server/job.lua

 end

 function JobManager.getGangs()
+    if GetResourceState("op-crime") == "started" then
+        local gangs = exports['op-crime']:getOrganisationsList()
+        return lib.array.map(gangs, function(g)
+            return {
+                name = g.identifier,
+                label = g.label,
+                grades = {}
+            }
+        end)
+    end
+
     if Framework.name == "esx" then
         return {}
     elseif Framework.name == "qb" or Framework.name == "qbx" then

bridge/server/player.lua

 ---@param playerObj table
 ---@return {name:string, label:string, grade: {name:string, level:number, isboss:boolean}} | nil
 function player.getGang(playerObj)
+    if GetResourceState("op_crime") == "started" then
+        local gangName = Player(playerObj.PlayerData.source).state.gangName
+        local gangId = Player(playerObj.PlayerData.source).state.gangId
+        local rank = Player(playerObj.PlayerData.source).state.gangRankName
+        return {
+            name = gangId,
+            label = gangName,
+            grade = {
+                name = rank,
+                level = rank,
+                isboss = false,
+            },
+        }
+    end
+
+
     if Framework.name == 'esx' then
         return nil -- ESX uses jobs for gangs
     elseif Framework.name == 'qb' or Framework.name == 'qbx' then

 function player.openClothingMenu(source)
     if GetResourceState("17mov_CharacterSystem") == "started" then
-        if Framework.name == "esx" then
-            TriggerClientEvent("esx_skin:openSaveableMenu", source)
-        else
-            TriggerClientEvent("qb-clothing:client:openMenuCommand", source)
-        end
+        TriggerClientEvent("esx_skin:openSaveableMenu", source)
+        TriggerClientEvent("qb-clothing:client:openMenuCommand", source)
     elseif GetResourceState('illenium-appearance') == "started" then
         TriggerClientEvent("illenium-appearance:client:openClothingShop", source, true)
     elseif GetResourceState('vms_clothestore') == "started" then
         TriggerClientEvent("qb-clothing:client:openMenu", source)
     elseif GetResourceState("0r-clothingv2") == "started" then
         TriggerClientEvent('0r-clothing:openCharacterCreationMenu', source, true, true)
+    elseif GetResourceState("um-clothing") == "started" then
+        if Framework.name == "esx" then
+            TriggerClientEvent("esx_skin:openSaveableMenu", source)
+        else
+            TriggerClientEvent("qb-clothing:client:openMenu", source)
+        end
     end
 end

config/config.json

     "__comment__": "Use luxu's built-in weather system"
   },
   "reports": {
-    "enabled": true
+    "enabled": true,
+    "video_record": true,
+    "voice_channel": true,
+    "transcripts": true,
+    "_comment_": "Set video_record, voice_channel, or transcripts to false to disable those report features."
   },
   "external_web_panel": {
     "enabled": true,
   },

   "logs": {
-    "_comment_": "Adjust as needed",
+    "_comment_": "Set discord_only to true to disable the panel logs page and database storage. Logs will only be sent to configured Discord channels.",
     "enabled": true,
+    "discord_only": false,
     "max_age_days": 7,
     "tag_discord_user": true,

config/database/queries.d.ts

   /** 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 matching an exact plate, with its owner's charId and
+   * model (hash for ESX/QB/QBX, model name for vRP) for display purposes.
+   * Uses an exact match (rather than a `LIKE` scan) so the lookup stays index-friendly.
+   */
+  getVehicleOwnersByPlate(plate: string): Promise<{ plate: string; char_id: string; model: string | number }[]>;
+
   /**
    * Returns the vehicle mod/properties data for a given plate.
    * ESX reads `vehicle` from `owned_vehicles`; QB/QBX reads `mods` from `player_vehicles`.

config/database/queries.js

     );
   },

+  /**
+   * Returns the vehicle matching an exact plate, with its owner's charId and
+   * model (hash for ESX/QB/QBX, model name for vRP) for display purposes.
+   * Uses an exact match (rather than a `LIKE` scan) so the lookup stays index-friendly.
+   * @param {string} plate
+   * @returns {Promise<{ plate: string, char_id: string, model: string | number }[]>}
+   */
+  async getVehicleOwnersByPlate(plate) {
+    if (!plate) return [];
+
+    if (Framework.name === 'esx') {
+      return await oxmysql.query(
+        `SELECT plate, owner as char_id, CAST(JSON_UNQUOTE(JSON_EXTRACT(vehicle, "$.model")) AS SIGNED) as model
+         FROM owned_vehicles WHERE plate = ? LIMIT 1`,
+        [plate]
+      );
+    }
+    if (Framework.name === 'vrp') {
+      const rows = await oxmysql.query('SELECT id FROM vrp_characters');
+      for (const row of rows) {
+        const vehicles = await Luxu.garage.getOwnedVehicles(String(row.id));
+        for (const vehicle of vehicles) {
+          if (String(vehicle.plate).toLowerCase() === plate.toLowerCase()) {
+            return [{ plate: vehicle.plate, char_id: String(row.id), model: Number(vehicle.model) }];
+          }
+        }
+      }
+      return [];
+    }
+
+    return await oxmysql.query(
+      'SELECT plate, citizenid as char_id, hash as model FROM player_vehicles WHERE plate = ? LIMIT 1',
+      [plate]
+    );
+  },
+
   /**
    * Returns the vehicle mod/properties data for a given plate.
    * ESX reads `vehicle` from `owned_vehicles`; QB/QBX reads `mods` from `player_vehicles`.

config/permissions.json

     "props.delete": "perm_props_delete",
     "props.view": "perm_props_view",
     "reports.close": "perm_reports_close",
+    "reports.transcript_view": "perm_reports_transcript_view",
+    "reports.transcript_delete": "perm_reports_transcript_delete",
     "resources.manage": "perm_resources_manage",
     "resources.read": "perm_resources_read",
     "self.blackout": "perm_self_blackout",
     "self.delete_vehicle": "perm_self_delete_vehicle",
     "self.entity_controls": "perm_self_entity_controls",
     "self.entity_inspector": "perm_self_entity_inspector",
+    "self.delete_entities": "perm_self_delete_entities",
     "self.freeze": "perm_self_freeze",
     "self.give_vehicle": "perm_self_give_vehicle",
     "self.godmode": "perm_self_godmode",

fxmanifest.lua


 author 'luxu-gg'
 description 'Admin menu for FiveM'
-version '1.14.7'
+version '1.14.9'

 escrow_ignore { 'config/**/*', 'bridge/**/*' }

sql/install.sql

   `claimed_by_name` varchar(50) DEFAULT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_luxu_admin_chats_luxu_admin_group_members` (`claimed_by`),
+  KEY `idx_report_closed_created` (`is_report`, `is_closed`, `created_at`),
   CONSTRAINT `FK_luxu_admin_chats_luxu_admin_group_members` FOREIGN KEY (`claimed_by`) REFERENCES `luxu_admin_group_members` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
   `type` varchar(64) NOT NULL,
   `data` longtext NOT NULL,
+  `unique_id` varchar(128) DEFAULT NULL,
+  `char_id` varchar(128) DEFAULT NULL,
+  `license` varchar(128) DEFAULT NULL,
+  `item_count` int(10) unsigned NOT NULL DEFAULT 0,
   `created_at` datetime NOT NULL DEFAULT current_timestamp(),
   PRIMARY KEY (`id`),
-  KEY `idx_type_created_at` (`type`, `created_at`)
+  KEY `idx_type_created_at` (`type`, `created_at`),
+  KEY `idx_snapshot_lookup` (`type`, `unique_id`, `char_id`, `license`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

 CREATE TABLE IF NOT EXISTS `luxu_admin_player_notes` (

Full list of files changed

Change File
Modified bridge/server/inventory.lua
Modified bridge/server/job.lua
Modified bridge/server/player.lua
Modified client/modules/actions.lua
Modified client/modules/chat.lua
Modified client/modules/clothingmenu.lua
Modified client/modules/entity_inspector.lua
Modified client/modules/new_clothing_menu.lua
Modified client/modules/props.lua
Modified config/config.json
Modified config/database/queries.d.ts
Modified config/database/queries.js
Modified config/permissions.json
Modified fxmanifest.lua
Modified server/modules/props.lua
Modified server/modules/snapshots.lua
Modified sql/install.sql
Modified server/index.js
Modified web/