v1.14.7
Summary
This release adds inventory snapshots with restore and delete actions, job/gang society balance viewing and management across many banking resources, a restructured staff logging system with richer metadata, enhanced duty tracking and statistics, Quick Menu reorder settings, and various inventory bridge and clothing fixes.
What's New
Features
- Inventory snapshots with configurable retention, automatic death-inventory capture, restore, and delete actions
- Job and gang society balance viewing and management with broad banking and boss menu resource support
- Restructured staff logs with permission, action, target, and metadata fields for better auditing
- Enhanced duty tracking, statistics, and Quick Menu action ordering
Bug Fixes
- Fixed job society balance detection and updates across supported banking resources
- Fixed 17mov CharacterSystem clothing menu routing for non-ESX frameworks
- Improved clothing skin reload priority for rcore, qs-appearance, and ak47 clothing resources
- Fixed Duty tracking issues
Server Hooks
New normalized server hooks for reports, bans, kicks, warns, and jail actions — available via Lua AddEventHandler or the registerHook export. Report payloads now use reportId instead of chat_id.
See Server Hooks for the full hook list, payloads, and examples.
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/client/clothing.lua
local clothing = {}
function clothing.loadPlayerSkin()
- if GetResourceState("qb-clothing") == "started" then
- TriggerServerEvent("qb-clothes:loadPlayerSkin")
+ if GetResourceState("rcore_clothing") == "started" or GetResourceState("qs-appearance") == "started" or GetResourceState("ak47_clothing") == "started" or GetResourceState("ak47_qb_clothing") == "started" then
+ ExecuteCommand("reloadskin")
elseif GetResourceState("illenium-appearance") == "started" then
local result = lib.callback.await("illenium-appearance:server:getAppearance")
exports["illenium-appearance"]:setPlayerAppearance(result)
- elseif GetResourceState("rcore_clothing") == "started" then
- ExecuteCommand("reloadskin")
+ elseif GetResourceState("qb-clothing") == "started" then
+ TriggerServerEvent("qb-clothes:loadPlayerSkin")
elseif GetResourceState("skinchanger") == "started" and GetResourceState("esx_skin") == "started" then
Framework.object.TriggerServerCallback('esx_skin:getPlayerSkin', function(skin)
TriggerEvent('skinchanger:loadSkin', skin)
end)
- elseif GetResourceState("qs-appearance") == "started" or GetResourceState("ak47_clothing") == "started" or GetResourceState("ak47_qb_clothing") == "started" then
- ExecuteCommand("reloadskin")
else
ExecuteCommand("reloadskin")
endbridge/server/inventory.lua
return GetResourceState(resource) == "started"
end
+local function getItemAmount(item)
+ if type(item) ~= "table" then return 0 end
+
+ local amount = tonumber(item.amount or item.count or item.quantity or 0) or 0
+ if amount <= 0 then return 0 end
+
+ return math.floor(amount)
+end
+
+local function normalizeItems(items)
+ local normalized = {}
+ if type(items) ~= "table" then return normalized end
+
+ for key, item in pairs(items) do
+ if type(item) == "table" and item.name then
+ local amount = getItemAmount(item)
+ if amount > 0 then
+ normalized[#normalized + 1] = {
+ name = tostring(item.name),
+ amount = amount,
+ slot = tonumber(item.slot) or tonumber(key),
+ metadata = item.metadata or item.info,
+ }
+ end
+ end
+ end
+
+ table.sort(normalized, function(a, b)
+ if a.slot and b.slot then return a.slot < b.slot end
+ if a.slot then return true end
+ if b.slot then return false end
+ return a.name < b.name
+ end)
+
+ return normalized
+end
+
---@param source number
---@return table<number, table>
function inventory.getPlayerItems(source)
end
---@param source number
-function inventory.addItem(source, item, amount, metadata)
+function inventory.addItem(source, item, amount, metadata, slot)
local playerObj = Luxu.player.getPlayerObject(source)
if not playerObj then return false end
if isResourceRunning('ox_inventory') then
- exports.ox_inventory:AddItem(source, item, amount, metadata, nil, nil)
+ exports.ox_inventory:AddItem(source, item, amount, metadata, slot, nil)
elseif isResourceRunning("origen_inventory") then
- exports.origen_inventory:AddItem(source, item, amount, nil, metadata)
+ exports.origen_inventory:AddItem(source, item, amount, slot, metadata)
elseif isResourceRunning('ak47_inventory') then
- exports['ak47_inventory']:AddItem(source, item, amount, nil, metadata)
+ exports['ak47_inventory']:AddItem(source, item, amount, slot, metadata)
elseif isResourceRunning('ak47_qb_inventory') then
- exports['ak47_qb_inventory']:AddItem(source, item, amount, nil, metadata)
+ exports['ak47_qb_inventory']:AddItem(source, item, amount, slot, metadata)
elseif isResourceRunning('core_inventory') then
local identifier = Luxu.player.getCharId(playerObj)
local inventory = 'content-' .. identifier:gsub(':', '')
exports['core_inventory']:addItem(inventory, item, amount, metadata, "content")
elseif isResourceRunning('qs-inventory') then
- exports['qs-inventory']:AddItem(source, item, amount, nil, metadata)
+ exports['qs-inventory']:AddItem(source, item, amount, slot, metadata)
elseif isResourceRunning("tgiann-inventory") then
exports["tgiann-inventory"]:AddItem(source, item, amount)
elseif isResourceRunning("codem-inventory") then
- exports["codem-inventory"]:AddItem(source, item, amount, nil, metadata)
+ exports["codem-inventory"]:AddItem(source, item, amount, slot, metadata)
elseif isResourceRunning("jaksam_inventory") then
- exports['jaksam_inventory']:addItem(source, item, amount, metadata)
+ exports['jaksam_inventory']:addItem(source, item, amount, metadata, slot)
else
if Framework.name == "qb" or Framework.name == "qbx" then
- playerObj.Functions.AddItem(item, amount, nil, metadata)
+ playerObj.Functions.AddItem(item, amount, slot, metadata)
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addInventoryItem(item, amount)
end
end
+function inventory.clearPlayerItems(source)
+ local playerObj = Luxu.player.getPlayerObject(source)
+ if not playerObj then return false end
+
+ for _, item in ipairs(normalizeItems(inventory.getPlayerItems(source))) do
+ local ok, result = pcall(inventory.removeItem, source, item.name, item.amount, item.slot)
+ if not ok or result == false then
+ return false
+ end
+ end
+
+ return true
+end
+
+function inventory.setPlayerItems(source, items)
+ local playerObj = Luxu.player.getPlayerObject(source)
+ if not playerObj then return false end
+
+ local normalized = normalizeItems(items)
+ if not inventory.clearPlayerItems(source) then
+ return false
+ end
+
+ for _, item in ipairs(normalized) do
+ local ok, result = pcall(inventory.addItem, source, item.name, item.amount, item.metadata, item.slot)
+ if not ok or result == false then
+ return false
+ end
+ end
+
+ return true
+end
+
---@return boolean
function inventory.canCarryItem(source, item, amount)
local playerObj = Luxu.player.getPlayerObject(source)bridge/server/player.lua
function player.openClothingMenu(source)
if GetResourceState("17mov_CharacterSystem") == "started" then
- TriggerClientEvent("17mov_CharacterSystem:OpenOutfitsMenu", source)
+ if Framework.name == "esx" then
+ TriggerClientEvent("17mov_CharacterSystem:OpenOutfitsMenu", source)
+ else
+ TriggerClientEvent("qb-clothing:client:openOutfitMenu", source)
+ end
elseif GetResourceState('illenium-appearance') == "started" then
TriggerClientEvent("illenium-appearance:client:openClothingShop", source, true)
elseif GetResourceState('vms_clothestore') == "started" thenbridge/server/society.lua
new file mode 100644
+local JobManager = require 'bridge.server.job'
+
+---@class SocietyManager
+local SocietyManager = {}
+
+local function isResourceStarted(resourceName)
+ return GetResourceState(resourceName) == 'started'
+end
+
+local function hasStartedResource(resourceNames)
+ for _, name in ipairs(resourceNames) do
+ if isResourceStarted(name) then return true end
+ end
+ return false
+end
+
+---@param frameworks string[]|nil
+local function matchesFramework(frameworks)
+ if not frameworks then return true end
+ for _, framework in ipairs(frameworks) do
+ if framework == Framework.name then return true end
+ end
+ return false
+end
+
+local function tableExists(tableName)
+ local ok, count = pcall(function()
+ return MySQL.scalar.await(
+ 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?',
+ { tableName }
+ )
+ end)
+ if not ok then return false end
+ return tonumber(count) > 0
+end
+
+local function normalizeExportBalance(value)
+ if type(value) == 'number' then return value end
+ if type(value) ~= 'table' then return nil end
+
+ for _, key in ipairs({ 'balance', 'amount', 'account_balance', 'money', 'newBalance' }) do
+ if value[key] ~= nil then return tonumber(value[key]) end
+ end
+
+ return nil
+end
+
+local function callResourceExport(resourceName, exportName, ...)
+ local ok, result = pcall(exports[resourceName][exportName], ...)
+ if ok then return result end
+ return nil
+end
+
+---@param resourceNames string[]
+local function getStartedExportResource(resourceNames)
+ for _, name in ipairs(resourceNames) do
+ if isResourceStarted(name) then return name end
+ end
+ return nil
+end
+
+---@param societyName string
+---@return string[]
+local function getSocietyNameVariants(societyName)
+ local rawName = tostring(societyName or ''):match('^%s*(.-)%s*$') or ''
+ local normalizedName = rawName:gsub('^society_', ''):match('^%s*(.-)%s*$') or ''
+ if normalizedName == '' then return {} end
+
+ local seen = {}
+ local names = {}
+
+ local function add(name)
+ if name ~= '' and not seen[name] then
+ seen[name] = true
+ names[#names + 1] = name
+ end
+ end
+
+ add(normalizedName)
+ add(rawName)
+ if not normalizedName:find('^society_') then add('society_' .. normalizedName) end
+
+ return names
+end
+
+---@param accountName string
+---@param prefixed boolean|nil
+---@return string[]
+local function getAccountNameVariants(accountName, prefixed)
+ local names = { accountName }
+ if prefixed then
+ if not accountName:find('^society_') then names[#names + 1] = 'society_' .. accountName end
+ if not accountName:find('^B') then names[#names + 1] = 'B' .. accountName:upper() end
+ end
+ return names
+end
+
+---@return table[]|nil
+local function getSocietyBalanceRowsFromTables()
+ local sources = {
+ {
+ resources = { 'okokBanking' },
+ table = 'okokbanking_societies',
+ query =
+ 'SELECT society as name, society_name as label, value as balance FROM okokbanking_societies ORDER BY value DESC',
+ },
+ {
+ resources = { 'tgg-banking' },
+ table = 'tgg_banking_accounts',
+ query =
+ "SELECT ownerId as name, name as label, balance FROM tgg_banking_accounts WHERE type = 'business' ORDER BY balance DESC",
+ },
+ {
+ resources = { 'vms_bossmenu' },
+ table = 'bossmenu',
+ query =
+ "SELECT job as name, account_name as label, balance FROM bossmenu WHERE type IN ('job', 'gang') ORDER BY balance DESC",
+ },
+ {
+ resources = { 'Renewed-Banking', 'renewed-banking', 'renewed_banking', 'qbx_management', 'qbx_core' },
+ frameworks = { 'qb', 'qbx' },
+ table = 'bank_accounts_new',
+ query = 'SELECT id as name, amount as balance FROM bank_accounts_new ORDER BY amount DESC',
+ },
+ {
+ resources = { 'wasabi_banking' },
+ frameworks = { 'qbx' },
+ table = 'bank_accounts_new',
+ query = 'SELECT id as name, amount as balance FROM bank_accounts_new ORDER BY amount DESC',
+ },
+ {
+ resources = { 'qb-banking', 'qb-management', 'wasabi_banking' },
+ frameworks = { 'qb' },
+ table = 'bank_accounts',
+ query =
+ "SELECT account_name as name, account_balance as balance FROM bank_accounts WHERE account_type IN ('job', 'gang') ORDER BY account_balance DESC",
+ },
+ {
+ resources = { 'qb-management', 'dw-bossmenu', 'dw_bossmenu', 'fd_banking', 'fd-banking', 'wasabi_banking' },
+ table = 'management_funds',
+ query =
+ "SELECT job_name as name, amount as balance FROM management_funds WHERE type IN ('boss', 'gang', 'job') ORDER BY amount DESC",
+ },
+ {
+ resources = { 'esx_management', 'esx_society', 'm-Banking', 'm-banking', 'mBanking', 'm_banking' },
+ table = 'addon_account_data',
+ query = [[SELECT REPLACE(account_name, 'society_', '') as name, money as balance
+ FROM addon_account_data
+ WHERE account_name LIKE 'society_%'
+ AND (owner IS NULL OR owner = '')
+ ORDER BY money DESC
+ LIMIT 100]],
+ },
+ {
+ resources = { 'wasabi_banking' },
+ frameworks = { 'esx' },
+ table = 'addon_account_data',
+ query = [[SELECT REPLACE(account_name, 'society_', '') as name, money as balance
+ FROM addon_account_data
+ WHERE account_name LIKE 'society_%'
+ AND (owner IS NULL OR owner = '')
+ ORDER BY money DESC
+ LIMIT 100]],
+ },
+ }
+
+ for _, source in ipairs(sources) do
+ if not matchesFramework(source.frameworks) then goto continue end
+ if not hasStartedResource(source.resources) then goto continue end
+ if not tableExists(source.table) then goto continue end
+
+ local ok, rows = pcall(function()
+ return MySQL.query.await(source.query)
+ end)
+ if ok and rows then return rows end
+
+ ::continue::
+ end
+
+ return nil
+end
+
+---@param accounts { name: string, label: string }[]
+---@return table[]|nil
+local function getSocietyBalanceRowsFromExports(accounts)
+ local sources = {
+ {
+ resources = { 'prism_banking', 'prism-banking' },
+ exportResource = 'prism_banking',
+ methods = { 'GetSocietyBalance' },
+ },
+ {
+ resources = { 'kartik-banking' },
+ exportResource = 'kartik-banking',
+ methods = { 'GetAccountMoney' },
+ },
+ {
+ resources = { 'g-bossmenu', 'g-management', 'g-banking', 'g-billing' },
+ exportResource = 'g-bossmenu',
+ methods = { 'GetSocietyMoney' },
+ },
+ {
+ resources = { 'bablo-banking' },
+ exportResource = 'bablo-banking',
+ methods = { 'GetSocietyBalance' },
+ prefixed = true,
+ },
+ {
+ resources = { 'tgg-banking' },
+ exportResource = 'tgg-banking',
+ methods = { 'GetSocietyAccountMoney' },
+ },
+ {
+ resources = { 'fd_banking' },
+ exportResource = 'fd_banking',
+ methods = { 'GetAccount', 'GetGangAccount', 'getBusinessAccount' },
+ },
+ }
+
+ for _, source in ipairs(sources) do
+ local activeResource = getStartedExportResource(source.resources)
+ if not activeResource then goto continue end
+
+ local exportResource = isResourceStarted(source.exportResource) and source.exportResource or activeResource
+ local rows = {}
+
+ for _, account in ipairs(accounts) do
+ local balance = nil
+
+ for _, accountName in ipairs(getAccountNameVariants(account.name, source.prefixed)) do
+ for _, method in ipairs(source.methods) do
+ balance = normalizeExportBalance(callResourceExport(exportResource, method, accountName))
+ if balance ~= nil then break end
+ end
+ if balance ~= nil then break end
+ end
+
+ if balance ~= nil then
+ rows[#rows + 1] = { name = account.name, label = account.label, balance = balance }
+ end
+ end
+
+ if #rows > 0 then
+ table.sort(rows, function(a, b) return a.balance > b.balance end)
+ return rows
+ end
+
+ ::continue::
+ end
+
+ return nil
+end
+
+---@param societyName string
+---@param value number
+---@return boolean
+local function updateSocietyBalanceInTables(societyName, value)
+ local balance = tonumber(value)
+ local names = getSocietyNameVariants(societyName)
+ if balance == nil or #names == 0 then return false end
+
+ local placeholders = string.rep('?,', #names):sub(1, -2)
+ local sources = {
+ {
+ resources = { 'okokBanking' },
+ table = 'okokbanking_societies',
+ query = ('UPDATE okokbanking_societies SET value = ? WHERE society IN (%s)'):format(placeholders),
+ },
+ {
+ resources = { 'tgg-banking', 'tgg_banking' },
+ table = 'tgg_banking_accounts',
+ query = ("UPDATE tgg_banking_accounts SET balance = ? WHERE type = 'business' AND ownerId IN (%s)"):format(
+ placeholders),
+ },
+ {
+ resources = { 'vms_bossmenu' },
+ table = 'bossmenu',
+ query = ("UPDATE bossmenu SET balance = ? WHERE type IN ('job', 'gang') AND job IN (%s)"):format(
+ placeholders),
+ },
+ {
+ resources = { 'Renewed-Banking', 'renewed-banking', 'renewed_banking', 'qbx_management', 'qbx_core' },
+ frameworks = { 'qb', 'qbx' },
+ table = 'bank_accounts_new',
+ query = ('UPDATE bank_accounts_new SET amount = ? WHERE id IN (%s)'):format(placeholders),
+ },
+ {
+ resources = { 'wasabi_banking' },
+ frameworks = { 'qbx' },
+ table = 'bank_accounts_new',
+ query = ('UPDATE bank_accounts_new SET amount = ? WHERE id IN (%s)'):format(placeholders),
+ },
+ {
+ resources = { 'qb-banking', 'qb-management', 'wasabi_banking' },
+ frameworks = { 'qb' },
+ table = 'bank_accounts',
+ query = ("UPDATE bank_accounts SET account_balance = ? WHERE account_type IN ('job', 'gang') AND account_name IN (%s)")
+ :format(
+ placeholders),
+ },
+ {
+ resources = { 'qb-management', 'dw-bossmenu', 'dw_bossmenu', 'fd_banking', 'fd-banking', 'wasabi_banking' },
+ table = 'management_funds',
+ query = ("UPDATE management_funds SET amount = ? WHERE type IN ('boss', 'gang', 'job') AND job_name IN (%s)")
+ :format(
+ placeholders),
+ },
+ {
+ resources = { 'esx_management', 'esx_society', 'm-Banking', 'm-banking', 'mBanking', 'm_banking' },
+ table = 'addon_account_data',
+ query = ([[UPDATE addon_account_data
+ SET money = ?
+ WHERE account_name IN (%s)
+ AND (owner IS NULL OR owner = '')]]):format(placeholders),
+ },
+ {
+ resources = { 'wasabi_banking' },
+ frameworks = { 'esx' },
+ table = 'addon_account_data',
+ query = ([[UPDATE addon_account_data
+ SET money = ?
+ WHERE account_name IN (%s)
+ AND (owner IS NULL OR owner = '')]]):format(placeholders),
+ },
+ }
+
+ local params = { balance }
+ for _, name in ipairs(names) do
+ params[#params + 1] = name
+ end
+
+ for _, source in ipairs(sources) do
+ if not matchesFramework(source.frameworks) then goto continue end
+ if not hasStartedResource(source.resources) then goto continue end
+ if not tableExists(source.table) then goto continue end
+
+ local ok, affected = pcall(function()
+ return MySQL.update.await(source.query, params)
+ end)
+ if ok and tonumber(affected) > 0 then return true end
+
+ ::continue::
+ end
+
+ return false
+end
+
+--- Returns job/gang society balances from supported banking and boss menu resources.
+---@return { name: string, label: string, balance: number }[]
+function SocietyManager.getJobSocietyBalances()
+ local societyLabels = {}
+ local accounts = {}
+
+ local function addAccount(account)
+ if not account or not account.name or societyLabels[account.name] then return end
+ societyLabels[account.name] = account.label or account.name
+ accounts[#accounts + 1] = { name = account.name, label = societyLabels[account.name] }
+ end
+
+ local ok, jobs = pcall(JobManager.getJobs)
+ if ok and jobs then
+ for _, job in ipairs(jobs) do addAccount(job) end
+ end
+
+ ok, jobs = pcall(JobManager.getGangs)
+ if ok and jobs then
+ for _, gang in ipairs(jobs) do addAccount(gang) end
+ end
+
+ local result = getSocietyBalanceRowsFromTables()
+ if not result then result = getSocietyBalanceRowsFromExports(accounts) end
+ if not result then return {} end
+
+ local balances = {}
+
+ for _, row in ipairs(result) do
+ local rawName = row.name or row.job or row.society or row.account_name or row.job_name
+ if not rawName then goto continue end
+
+ local name = tostring(rawName):gsub('^society_', ''):match('^%s*(.-)%s*$') or ''
+ if name == '' then goto continue end
+
+ local balance = tonumber(row.balance)
+ local item = {
+ name = name,
+ label = row.label or societyLabels[name] or name,
+ balance = balance or 0,
+ }
+
+ local existing = balances[name]
+ if not existing or item.balance > existing.balance then
+ balances[name] = item
+ end
+
+ ::continue::
+ end
+
+ local sorted = {}
+ for _, item in pairs(balances) do
+ sorted[#sorted + 1] = item
+ end
+
+ table.sort(sorted, function(a, b) return a.balance > b.balance end)
+ return sorted
+end
+
+--- Sets a job/gang society balance in supported banking and boss menu tables.
+---@param societyName string
+---@param value number
+---@return boolean
+function SocietyManager.updateSocietyBalance(societyName, value)
+ return updateSocietyBalanceInTables(societyName, value)
+end
+
+return SocietyManagerconfig/config.json
"images_url": "https://cfx-nui-qb-inventory/html/images/%s.png",
"_comment_": "If auto_detect is true, the inventory will be detected automatically. If it is false, you need to specify the inventory resource name."
},
+ "snapshots": {
+ "retention_days": 3,
+ "cleanup_interval_minutes": 60,
+ "death_inventory": {
+ "enabled": true,
+ "min_delay_seconds": 60
+ }
+ },
"vehicles": {
"use_ox_lib_for_mods": false,
"images_url": "https://docs.fivem.net/vehicles/%s.webp",config/hooks/server.lua
}
end)
---- Uncomment this if you need
+--- Uncomment these if you need
-- Report Created
-- AddEventHandler("luxu_admin:server:report_created", function(data)
+-- local chatId = data.reportId --[[@as number]]
-- local authorSource = data.author_source --[[@as number]]
-- local authorName = data.author_name --[[@as string]]
-- end)
+
+-- Player Banned
+-- AddEventHandler("luxu_admin:server:player_banned", function(data)
+-- local banId = data.banId --[[@as number]]
+-- local playerName = data.playerName --[[@as string]]
+-- local reason = data.reason --[[@as string]]
+-- local staffId = data.staffId --[[@as number|nil]]
+-- local staffName = data.staffName --[[@as string|nil]]
+-- end)config/permissions.json
"offline_player.update_name": "perm_offline_player_update_name",
"vehicles.force_wipe": "perm_vehicles_force_wipe",
"vehicles.view_players_inside": "perm_vehicles_view_players_inside",
- "map:view_vehicles": "perm_map_view_vehicles"
+ "map:view_vehicles": "perm_map_view_vehicles",
+ "society.view_balance": "perm_society_view_balance",
+ "society.manage_balance": "perm_society_manage_balance"
}
}fxmanifest.lua
author 'luxu-gg'
description 'Admin menu for FiveM'
-version '1.14.6'
+version '1.14.7'
dependencies {
'oxmysql',
files { 'config/config.json', 'config/themes.css', 'config/permissions.json', 'config/sounds/*', 'config/help.lua', 'config/constants/*.json', 'config/logging/cl_logs.lua', 'config/teleports.lua', 'config/commands/cl_commands.lua', 'config/custom_functions/client.lua', 'locales/*.json', "web/index.html", 'web/**/*', "bridge/framework.lua", "bridge/shared/**/*.lua", "bridge/client/**/*.lua", "bridge/vrp2/**/*.lua" }
-ui_page 'web/index.html'
---ui_page 'http://localhost:7017/'
+--ui_page 'web/index.html'
+ui_page 'http://localhost:7017'
escrow_ignore { 'config/**/*', 'bridge/**/*' }sql/install.sql
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+CREATE TABLE IF NOT EXISTS `luxu_admin_snapshots` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `type` varchar(64) NOT NULL,
+ `data` longtext NOT NULL,
+ `created_at` datetime NOT NULL DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ KEY `idx_type_created_at` (`type`, `created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
CREATE TABLE IF NOT EXISTS `luxu_admin_player_notes` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`player_id` varchar(255) NOT NULL,
CREATE TABLE IF NOT EXISTS `luxu_admin_staff_logs` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `staff_id` int(10) unsigned NOT NULL,
+ `staff_id` int(10) unsigned DEFAULT NULL,
`staff_name` varchar(255) NOT NULL,
- `text` text NOT NULL,
+ `permission` varchar(255) DEFAULT NULL,
+ `action` varchar(100) NOT NULL,
+ `target_type` varchar(50) DEFAULT NULL,
+ `target_id` varchar(255) DEFAULT NULL,
+ `target_name` varchar(255) DEFAULT NULL,
+ `metadata` longtext NOT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_staff_created` (`staff_id`, `created_at` DESC),
+ KEY `idx_permission_created` (`permission`, `created_at` DESC),
+ KEY `idx_action_created` (`action`, `created_at` DESC),
+ KEY `idx_target_created` (`target_type`, `target_id`, `created_at` DESC),
KEY `idx_created` (`created_at` DESC),
- CONSTRAINT `fk_staff_logs_staff` FOREIGN KEY (`staff_id`) REFERENCES `luxu_admin_group_members` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+ CONSTRAINT `fk_staff_logs_staff` FOREIGN KEY (`staff_id`) REFERENCES `luxu_admin_group_members` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `luxu_admin_player_coords_history` (Full list of files changed
| Change | File |
|---|---|
| Modified | bridge/client/clothing.lua |
| Modified | bridge/server/inventory.lua |
| Modified | bridge/server/player.lua |
| Added | bridge/server/society.lua |
| Modified | client/cl_main.lua |
| Modified | client/modules/actions.lua |
| Modified | client/modules/nui.lua |
| Added | client/modules/snapshots.lua |
| Modified | config/config.json |
| Modified | config/hooks/server.lua |
| Modified | config/permissions.json |
| Modified | fxmanifest.lua |
| Added | server/modules/snapshots.lua |
| Modified | server/index.js |
| Modified | server/utils/sv_functions.lua |
| Modified | sql/install.sql |
| Modified | web/ |