# Game Management — documentation

> A web admin panel for your staff: offline players, the economy and an audit trail.

- URL: https://xexstudio.com/docs/game-management
- Version: 2.2.0
- Product page: https://xexstudio.com/scripts/game-management
## Overview

XeX Panel connects to the same MySQL/MariaDB database your FiveM server uses and gives your staff a web interface to manage it.

- No FiveM resource required: the panel works on the database. The Live page and the online indicator read your server's HTTP API.
- One adapter (`includes/framework.php`) handles ESX, QBCore and Qbox, including the JSON columns QBCore and Qbox use.
- Optional resources (housing, banking, phone, billing, ox_inventory) are detected from your schema. Pages and menu entries for resources you do not run are hidden.
- Role-based access with per-role page and action permissions.
- CSRF tokens, prepared statements, output escaping and login rate limiting.
- Audit log of staff actions, plus webhooks to Discord, Slack or any JSON endpoint.

---

## Supported Frameworks

| Framework | Player table | Notes |
| --------- | ------------ | ----- |
| **ESX Legacy** | `users` | Plain columns (`firstname`, `job`, `job_grade`, …) and an `accounts` JSON column. |
| **QBCore** | `players` | JSON columns `charinfo`, `job`, `gang`, `money`, `metadata`. |
| **Qbox** (qbx_core) | `players` | Same layout as QBCore, plus `player_groups`, which the panel keeps in sync when a job changes. |
| **Custom** | configurable | Treated as an ESX-like layout. Map your names in `$DB_TABLES` (see [Configuration](#table-and-column-overrides)). |

### Framework detection

The installer inspects your database:

1. `users` table with `identifier` and `accounts` columns → **ESX**
2. `players` table with `citizenid` and `charinfo` columns → **QBCore**, or **Qbox** when a `player_groups` table also exists
3. Otherwise → **Custom**

You can override the detected value on the installer's framework step (ESX, QBCore, Qbox or Custom), or later by editing `$FRAMEWORK` in `config.php`.

### How QBCore and Qbox data is handled

- Names, job, gang and licences are read with `JSON_EXTRACT` and written with `JSON_SET`, so only the edited keys change. The rest of `charinfo`, `job` and `metadata` is left as it was.
- Money is written as JSON numbers (not strings) inside the `money` column.
- Licences are stored in `players.metadata.licences` (`driver`, `weapon`, `business`, plus any keys you add in Settings).
- Changing a job sets `job.name`, `job.grade.level` and `job.onduty = false`. qb-core / qbx_core refresh the job label, grade name and payment from their shared jobs file when the player loads.
- **Qbox:** on a job change the `player_groups` job row is replaced (or removed when the new job is the default "unemployed" job).
- Job grades live in `shared/jobs.lua`, not in the database, so the Job Grades page shows jobs in use and their headcount instead.

---

## Optional Resources

The panel reads the table list once and caches it for about 2 minutes (`cache/schema.json`). For each feature it uses the first source that exists. **Settings → System → Detected resources** lists what was found, and **Rescan** clears the cache.

| Feature | ESX / Custom | QBCore / Qbox |
| ------- | ------------ | ------------- |
| Housing | `$DB_TABLES['housing']` if set, then loaf_housing, esx_property (`owned_properties`), `properties` | ps-housing / qbx_properties (`properties`), qb-houses (`player_houses`) |
| Business funds | `addon_account_data` (labels from `addon_account`) | qb-management (`management_funds`), qb-banking (`bank_accounts`), Renewed-Banking (`bank_accounts_new`) |
| Billing | `billing` | qb-phone (`phone_invoices`) |
| ox_inventory stashes | `ox_inventory` | `ox_inventory` |
| Phone | `phone_phones` (or `$DB_TABLES['phone']`) | `phone_phones` (or `$DB_TABLES['phone']`) |
| Job grades | `job_grades` | not in the database (`shared/jobs.lua`) |
| Licences | `user_licenses` | `players.metadata.licences` |
| Skins / outfits | `users.skin`, `datastore_data` ("property") | `playerskins`, `player_outfits` |
| Premium / VIP | only if `$DB_TABLES['premium']` is set and the table exists | same |

When a source is missing, its menu entry, page sections, dashboard cards and table columns are hidden.

---

## Features

### Core Management

| Feature | Description |
| ------- | ----------- |
| **Dashboard** | Money per account, business funds, unique users, characters, vehicles, houses, societies, items, open anomaly alerts and recent staff actions. Only cards for data your server has are shown. |
| **Players** | Player list with search and sort; player profile with editable identity, job, accounts, inventory, skin and licences. |
| **Vehicles** | Vehicle counts, models and garages; delete by plate, send to garage, delete job vehicles (ESX). |
| **Societies** | Job and gang funds, members with grades, and society inventories (ESX). |
| **Housing** | Property list with owners, from the detected housing resource. |
| **Job Grades** | ESX: jobs, grades and salaries. QBCore/Qbox: jobs in use and headcount. |
| **Licenses** | Grant and revoke licences (ESX `user_licenses`, QB/Qbox `metadata.licences`). |
| **Item Finder** | Find who holds an item across inventories, stashes, trunks and gloveboxes. |
| **Live Server** | Connected players and server info from your server's API URL. |

### Advanced Operations

| Feature | Description |
| ------- | ----------- |
| **Character Kill (CK)** | Deletes a character and its rows in the related tables that exist on your server. |
| **Swap** | ESX: moves all data from one identifier to another. QBCore/Qbox: moves a character to another Rockstar license. |
| **Quick search** | `Ctrl+K` / `Cmd+K` from any page: players (name or identifier), plates and panel pages. |

### Intelligence Suite

| Feature | Description |
| ------- | ----------- |
| **Economy** | Totals, wealth distribution, top richest players, money by job, cash-heavy players. |
| **Business Analytics** | Job headcount, society funds, activity per job, abandoned businesses, grade distribution. |
| **Anomaly Detection** | Alerts for extreme wealth and negative balances, with a dismiss workflow. |
| **Analytics** | Money per account over time (needs the cron job). |
| **Staff Activity** | Staff actions over time, hourly heatmap, category breakdown, ranking. |

### Administration

| Feature | Description |
| ------- | ----------- |
| **Audit Log** | Staff action trail with filters, pagination, CSV export and purge. |
| **Webhooks** | Multiple endpoints with Discord embeds, Slack attachments or generic JSON. |
| **Settings** | 8 tabs: General, Appearance, Game Config, Economy, Limits, Staff, Permissions, System. |
| **Permissions** | Per-role page and action permissions stored in the database, plus custom roles. |
| **Profile** | Change your own password. |

### Interface

- Dark theme with a configurable accent colour and logo.
- Sidebar grouped into **Overview**, **Players**, **World**, **Insights** and **Admin**. It collapses on desktop (the state is remembered) and becomes a drawer on mobile.
- Top bar with the page title, quick search, profile and logout.
- Skin viewer grouped into sections (Identity, Hair, Face, Makeup, Clothing, Damage).
- Inventory, stash, trunk and glovebox contents shown with item labels and counts.
- Charts drawn with Chart.js.

---

## Requirements

### Server

| Component | Minimum | Notes |
| --------- | ------- | ----- |
| **PHP** | 7.4 | Checked by the installer. PHP 8.1+ is recommended for security support. |
| **MySQL** | 5.7.8 | JSON functions are required. |
| **MariaDB** | 10.2.7 | JSON functions and the `JSON` column alias are required. |
| **Web server** | Apache 2.4 with `mod_rewrite` and `mod_headers`, or Nginx + PHP-FPM | The shipped protection rules are in `.htaccess` (Apache only). |

The panel uses these SQL JSON functions: `JSON_EXTRACT`, `JSON_UNQUOTE`, `JSON_SET`, `JSON_OBJECT` and `JSON_VALID`.

### PHP Extensions

| Extension | Purpose |
| --------- | ------- |
| `pdo_mysql` | Database access (required) |
| `json` | JSON columns, settings, cache (required) |
| `session` | Login sessions (required) |
| `mbstring` | Name initials in the sidebar, quick search, skin viewer. The installer lists it as optional, but install it. |
| `fileinfo` | Logo upload type check |
| `curl` | Live page requests (falls back to `file_get_contents` when missing) |

The panel directory and `cache/` must be writable by the web server during installation (the installer writes `config.php`). `assets/img/` and `assets/data/` must be writable for logo and item catalogue uploads.

### Supported Environments

- **XAMPP** (Windows) for local or small setups
- **LAMP** (Apache + PHP + MySQL/MariaDB) on Linux
- **Nginx + PHP-FPM**: port the `.htaccess` rules to your server block (see [Security](#file-protection))
- **Shared hosting** with PHP 7.4+ and MySQL/MariaDB

---

## Installation

### Method 1: Installer (recommended)

#### 1. Copy the files

| Environment | Path |
| ----------- | ---- |
| XAMPP (Windows) | `C:\xampp\htdocs\xex-panel\` |
| Linux / Apache | `/var/www/html/xex-panel/` |
| Nginx | Your configured web root |

#### 2. Open the installer

```
http://your-server/xex-panel/install/
```

The installer has six steps:

1. **Requirements**: PHP version, extensions, writable directories.
2. **Database**: host, user, password and database name of your FiveM database. The host may include a port, for example `127.0.0.1:3307`.
3. **Framework**: the detected framework is preselected. Choose ESX, QBCore, Qbox or Custom.
4. **Review**: lists the panel tables that will be created and the ones that already exist. Game tables are not modified.
5. **Admin account and panel settings**: superadmin username, email and password, panel name and FiveM API URL.
6. **Done**: shows the result for each table.

The installer creates the panel tables with `CREATE TABLE IF NOT EXISTS` (seed rows with `INSERT IGNORE`), writes `config.php` and creates `install/installed.lock`.

> **Password policy:** at least 8 characters with an uppercase letter, a lowercase letter and a number.

#### 3. Log in

```
http://your-server/xex-panel/
```

Then delete or block the `install/` folder.

---

### Method 2: Manual installation

#### 1. Import the schema

Import `setup/schema.sql` into your **FiveM database** (the panel tables live next to your game tables):

```bash
mysql -u root -p your_database < setup/schema.sql
```

#### 2. Create `config.php`

`config.php` is not shipped; the installer normally writes it. Create it in the panel root with this content:

```php
<?php
$MODE = "production"; // "development" shows PHP errors

if ($MODE === 'production') {
    error_reporting(0);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
} else {
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
}

// ─── Database (host may include a port: 127.0.0.1:3307) ───
$DDBB_HOST = 'localhost';
$DDBB_USER = 'root';
$DDBB_PASSWORD = 'your_password';
$DATABASE_NAME = 'your_database';
$HOST = '';

// ─── Framework: "esx" | "qbcore" | "qbox" | "custom" ───
$FRAMEWORK = 'esx';

// ─── Table and column overrides (empty = framework defaults) ───
$DB_TABLES = [];

// ─── Panel ───
$PANEL_NAME = 'Admin Panel';
$API_FIVEM_URL = '';
$WEBHOOK_URL = '';

require_once __DIR__ . '/includes/bootstrap.php';
```

#### 3. Create an admin account

```sql
INSERT INTO superadmins (username, password, email, role, force_password_change)
VALUES ('admin', '$2y$10$your_bcrypt_hash', 'admin@example.com', 3, 1);
```

Generate the hash with `php -r "echo password_hash('your_password', PASSWORD_DEFAULT);"`. With `force_password_change = 1` the panel asks for a new password after the first login.

---

### Post-installation

#### Live page and online status

Set **Settings → General → FiveM API URL**. The value from `$API_FIVEM_URL` is only the starting default. Accepted responses:

- the cfx.re server API: `https://servers-frontend.fivem.net/api/servers/single/YOUR_SERVER_ID/` (players, server variables and resources)
- a plain player list such as your server's `http://IP:30120/players.json`, or any JSON with a `players` array

The player profile uses the same URL to show whether the character's Rockstar license is online. When the URL is empty, the Live menu entry has nothing to show.

#### Item labels

Upload your item list in **Settings → Game Config → Item catalogue**:

- `ox_inventory/data/items.lua`
- `qb-core/shared/items.lua` (or the Qbox equivalent)
- or a JSON file: `{"water": "Water"}`, `{"water": {"label": "Water"}}` or `[{"name": "water", "label": "Water"}]`

The file (up to 4 MB) is parsed and stored as `assets/data/items.json`. Without an upload the panel uses the ESX `items` table, and otherwise the item names found in player inventories. **Use automatic source** removes the uploaded catalogue.

#### Analytics cron job (optional)

`setup/cronjob_money_analytics.php` stores one snapshot per account that exists on your server (ESX: bank, cash, black money; QBCore/Qbox: bank, cash, crypto; plus any custom currency you configured).

**Linux (crontab):**
```bash
0 */6 * * * php /var/www/html/xex-panel/setup/cronjob_money_analytics.php
```

**Windows (Task Scheduler):**
1. Create Basic Task → name `XeX Panel Analytics`
2. Trigger: daily, repeat every 6 hours
3. Action: Start a program
   - Program: `C:\xampp\php\php.exe`
   - Arguments: `C:\xampp\htdocs\xex-panel\setup\cronjob_money_analytics.php`

Without the cron job the Analytics page has no data.

---

## Upgrading from 2.1

1. Back up your database and the panel folder.
2. Replace **all files** with the 2.2.0 files, **except**:
   - `config.php`
   - `install/installed.lock`
   - `assets/img/custom-logo.*` (if present)
   - `assets/data/items.json` (if present)
3. Open the panel. A `config.php` generated by 2.1 keeps working: the 2.2 bootstrap and security files only define values the old config does not already set.
4. Optional: upload your `items.lua` in **Settings → Game Config** (the item file bundled with 2.1 is no longer used) and check **Settings → System → Detected resources**.

### Optional: switch to the shorter 2.2 config

In 2.2, `config.php` only holds values (database, framework, `$DB_TABLES` overrides, panel name, FiveM API URL) and ends with `require_once __DIR__ . '/includes/bootstrap.php';`. Defaults per framework live in `includes/framework.php`. To regenerate it:

1. Copy your current `config.php` somewhere safe.
2. Delete `install/installed.lock` and open `/install/`.
3. Go through the steps. Existing panel tables are kept (`CREATE TABLE IF NOT EXISTS`, `INSERT IGNORE`), and the installer overwrites `config.php`.
4. The installer also creates a superadmin account. Use a username that does not exist yet, or the step fails.
5. The new config has an empty `$DB_TABLES`. If your old config mapped anything the defaults do not cover (a custom currency key, a premium table, a renamed table), copy those keys into the new `$DB_TABLES`.
6. Delete or block the `install/` folder again.

---

## Configuration

Values live in `config.php`. Most runtime options are in the **Settings** page.

### Application mode

```php
$MODE = "production";   // errors logged only (use this in production)
$MODE = "development";  // errors shown on screen
```

### Database

```php
$DDBB_HOST = 'localhost';        // or '127.0.0.1:3307' for a non-default port
$DDBB_USER = 'root';
$DDBB_PASSWORD = 'your_password';
$DATABASE_NAME = 'your_database';
```

### Framework

```php
$FRAMEWORK = 'esx';      // ESX Legacy
$FRAMEWORK = 'qbcore';   // QBCore
$FRAMEWORK = 'qbox';     // Qbox
$FRAMEWORK = 'custom';   // ESX-like layout with your own names in $DB_TABLES
```

### Table and column overrides

`$DB_TABLES` is for overrides only. Leave it empty unless your server renames something. Defaults are in `fw_default_tables()` in `includes/framework.php`.

```php
$DB_TABLES = [
    'account_black' => 'dirty_money',   // ESX account key for black money
    'account_coin'  => 'coins',         // extra currency stored in accounts/money JSON
    'housing'       => 'my_houses',     // ESX / Custom: housing table
    'housing_owner' => 'owner',
    'premium'       => 'my_premium',    // optional premium/VIP table
];
```

<details>
<summary><strong>ESX / Custom keys and defaults</strong></summary>

| Key | Default | Meaning |
| --- | ------- | ------- |
| `users` | `users` | Player table |
| `users_identifier` | `identifier` | Player key |
| `users_firstname` / `users_lastname` | `firstname` / `lastname` | Name columns |
| `users_accounts` | `accounts` | Accounts JSON column |
| `users_group` | `group` | Admin group |
| `users_job` / `users_job_grade` | `job` / `job_grade` | Job columns |
| `users_inventory` | `inventory` | Inventory JSON |
| `users_skin` | `skin` | Skin JSON |
| `users_pincode` | `pincode` | Pincode column |
| `users_created_at` / `users_last_seen` | `created_at` / `last_seen` | Dates |
| `account_bank` / `account_cash` / `account_black` | `bank` / `money` / `black_money` | Keys inside `accounts` |
| `account_coin` | none | Optional extra currency key inside `accounts` |
| `vehicles`, `vehicles_owner`, `vehicles_plate` | `owned_vehicles`, `owner`, `plate` | Vehicles |
| `licenses`, `licenses_owner`, `licenses_type` | `user_licenses`, `owner`, `type` | Licences |
| `job_grades`, `jobs`, `items` | `job_grades`, `jobs`, `items` | Jobs and items |
| `societies`, `society_data` | `addon_account`, `addon_account_data` | Society accounts |
| `billing`, `datastore`, `inventory_items` | `billing`, `datastore_data`, `addon_inventory_items` | Other ESX tables |
| `ox_inventory` | `ox_inventory` | ox_inventory stashes |
| `housing`, `housing_owner` | auto-detected | Housing table |
| `phone` | auto-detected (`phone_phones`) | Phone table (needs an `id` column) |
| `premium` | none | Premium table (needs an `identifier` column with the license hash) |

A column that does not exist on your table is skipped, and the field is hidden in the UI.

</details>

<details>
<summary><strong>QBCore / Qbox keys and defaults</strong></summary>

The `players` JSON layout (`charinfo`, `job`, `gang`, `money`, `metadata`) is fixed. These keys can still be overridden:

| Key | Default | Meaning |
| --- | ------- | ------- |
| `users` | `players` | Player table |
| `users_accounts` | `money` | Money JSON column |
| `users_inventory` | `inventory` | Inventory JSON |
| `users_last_seen` | `last_updated` | Last seen |
| `account_bank` / `account_cash` | `bank` / `cash` | Keys inside `money` |
| `account_coin` | `crypto` | Shown as the extra currency ("Crypto") |
| `vehicles` | `player_vehicles` | Vehicles table |
| `skins` | `playerskins` | Skins table |
| `ox_inventory` | `ox_inventory` | ox_inventory stashes |
| `phone`, `premium` | as above | Optional tables |

ESX-only keys (names, group, job columns, licences table, job grades, societies, billing, datastore, black money, housing) are ignored on QBCore/Qbox, so an old config that mapped them to `charinfo` cannot write plain text into a JSON column.

</details>

### Accounts

| Framework | Accounts shown |
| --------- | -------------- |
| ESX | Bank (`bank`), Cash (`money`), Black money (`black_money`) |
| QBCore / Qbox | Bank (`bank`), Cash (`cash`), Crypto (`crypto`) as the extra currency |

Set `account_coin` in `$DB_TABLES` to show a custom currency stored in the accounts/money JSON. Display labels are editable in **Settings → Game Config → Account Labels**. The extra currency is not counted in wealth totals.

### General settings (Settings → General)

| Setting | Default |
| ------- | ------- |
| Panel name | `Admin Panel` (or the value from the installer) |
| FiveM API URL | value of `$API_FIVEM_URL` |
| Currency symbol / position | `$` / before |
| Session timeout (seconds) | `3600` |
| Cache TTL (seconds) | `300` |
| Max login attempts | `5` |
| Login lockout time (seconds) | `900` |
| DataTable page sizes | `20,50,100` |

### Appearance (Settings → Appearance)

- **Accent colour**: any `#rrggbb`. Text drawn on the accent switches between dark and light automatically for contrast.
- **Logo**: PNG, JPG or WebP, up to 512 KB, stored as `assets/img/custom-logo.*` and also used as the favicon. Without a logo the sidebar shows the initials of the panel name.

### Game Config (Settings → Game Config)

| Setting | Description |
| ------- | ----------- |
| **Item catalogue** | Upload `items.lua` or JSON for item labels |
| **Account Labels** | Names shown for bank, cash, black money and the extra currency |
| **License Types** | Licence keys offered when granting (ESX defaults: `dmv`, `drive`, `drive_bike`, `drive_truck`, `weapon`; QB defaults: `driver`, `weapon`, `business`) |
| **Job Configuration** | Default "unemployed" job name used by job reset and Qbox group sync; job list for the ESX job-vehicle delete form |

### Economy (Settings → Economy)

| Setting | Default |
| ------- | ------- |
| Wealth multiplier (Economy page) | `5` |
| Std-dev multiplier (Anomalies page) | `3` |
| Cash-heavy ratio / minimum total | `0.8` / `10000` |
| Severity: critical / high (× average) | `20` / `10` |
| Alert dedup window (hours) | `24` |
| Salary and society colour thresholds | `5000`/`1000`, `1000000`/`100000` |
| Active / inactive employee window (days) | `7` / `30` |
| Wealth brackets | `<1K` … `>1M` |

### Limits (Settings → Limits)

Row counts for the Economy, Anomalies, Business, Staff Activity and Webhook pages, and for quick search results (players `8`, vehicles `5`).

---

## Panel Modules

### Dashboard (`home.php`)

- Money totals per account that exists on your server, plus business funds
- Unique users (by Rockstar license) and characters
- Vehicles, houses, societies and item counts, when those sources exist
- Active premium rows, only when a premium table is configured
- Open anomaly alerts and the last 8 staff actions (for roles that can see those pages)

### Players (`users.php` → `view-user.php`)

**List:** search and sort with DataTables. Columns follow your framework (group on ESX, gang on QB, black money and extra currency when present). The list is cached in `cache/users_<framework>.json` and cleared after player edits.

**Profile:**
- **Report card:** total wealth, accounts, vehicle, house and licence counts, online status, staff tags and notes
- **Player information:** name, job and grade, group and pincode (ESX), inventory JSON, skin JSON. On ESX the job is checked against `job_grades`.
- **Accounts:** editable balances for the accounts your server has
- **Licences:** grant and revoke
- **Data explorer tabs:** vehicles (with trunk and glovebox), houses, inventories (ox_inventory stashes), phone, outfits, premium, each shown only when the source exists

Edits require the matching action permission and a CSRF token.

### Vehicles (`vehicles.php`)

- Total, stored and out/impounded counts, models and garages
- Delete a vehicle by plate (all frameworks)
- Send a vehicle to a garage by plate: ESX sets `parking`, `stored = 1`, clears `pound`; QBCore/Qbox set `garage`, `state = 1`, `depotprice = 0` (each column only if it exists)
- Delete a player's job vehicles (ESX only, needs a `job` column on `owned_vehicles`)
- ESX model hashes are resolved through the esx_vehicleshop `vehicles` table when present

### Societies (`societies.php` → `view-societies.php`)

- Funds from the detected business-funds source (see [Optional Resources](#optional-resources))
- Members of the job or gang with grade and last seen
- ESX: society ox_inventory stash, addon inventory and datastore weapons

### Houses (`houses.php`)

- Properties with owner name and, when available, shell/interior/tier
- Hidden when no housing resource is detected

### Job Grades (`grades.php`, linked from Societies)

- ESX: job name, grade, label and salary from `job_grades`
- QBCore/Qbox: jobs currently held by players, with headcount

### Item Finder (`itemsFinder.php` → `search-items.php`)

- Lists the item catalogue
- Searches player inventories (and ESX loadouts), ox_inventory stashes, ESX addon inventories and datastores, vehicle trunk/glovebox columns, and qb-inventory tables (`stashitems`, `trunkitems`, `gloveboxitems`, `inventories`) when they exist
- Shows owner, location and count

### Live Server (`live.php`)

- Connected players from the configured API URL (name, server ID, ping, license), with a link that searches the players list by license
- When the API cannot be reached, the page shows the error it received
- Max clients, project name/description and resources when the cfx.re API is used

### Character Kill (`ck_user.php` → `makeck.php`)

Deletes the player row last, inside a transaction, after these related rows (each table only if it exists):

| ESX / Custom (by identifier) | QBCore / Qbox (by citizenid) |
| ---------------------------- | ---------------------------- |
| player-owned `addon_account_data`, `addon_inventory_items`, `datastore_data` rows | `player_vehicles` |
| `billing`, `owned_vehicles`, `user_licenses` | `playerskins`, `player_outfits` |
| housing, phone and premium rows | `player_houses`, `phone_invoices` |
| `ox_inventory` rows owned by the player | `ox_inventory` rows owned by the player, `player_groups` (Qbox) |

Enter the ESX identifier or the QB/Qbox citizenid.

### Swap (`swaps.php` → `controllers/swap-controller.php`)

- **ESX / Custom:** moves everything from the old identifier to the new one. It first runs a CK on the new identifier, then updates the identifier in the player table, vehicles, licences, housing, ox_inventory, addon accounts/inventories, datastores and billing (identifier, sender, target). Swapping an identifier onto itself is refused.
- **QBCore / Qbox:** moves a character (citizenid) to another Rockstar license (`license:<hash>` or the bare hash). Nothing is deleted. It updates `players.license`, assigns the next free `cid` on the new license, updates `player_vehicles.license`, and on Qbox sets `players.userId` to the matching `users` row.

### Player export (`export-players.php`)

- **Export CSV** on the Players page downloads every character with identifier, name, job, grade, gang (QB), each account and last seen
- UTF-8 with BOM so Excel keeps accents; cells that start with `=`, `+`, `-` or `@` are prefixed to prevent formula injection
- Requires the `export_players` action

### Demo mode

Set `$DEMO_MODE = true;` in `config.php` to run a read-only panel, for example a public demo with fake data. Every form submission except signing in and item search is refused before it reaches a controller, and the top bar shows **Demo · read-only**. `$DEMO_LOGIN = ['demo', 'demo'];` shows those credentials on the sign-in page. Create that account as a normal staff account first.

### Analytics (`analytics.php`)

- One chart per account, each on its own scale, with the latest total and the change over 30 days
- Several snapshots on the same day are reduced to the last one

---

## Role & Permission System

### Built-in roles

| Role | Level |
| ---- | ----- |
| **Viewer** | 0 |
| **Moderator** | 1 |
| **Admin** | 2 |
| **Superadmin** | 3 |

These four system roles cannot be deleted. Superadmins can create custom roles (name, level, colour) on the **Permissions** page and grant pages and actions per role. Permissions saved there replace the defaults below for that role. Superadmins have access to everything.

### Default page access

| Page | Viewer | Moderator | Admin | Superadmin |
| ---- | :----: | :-------: | :---: | :--------: |
| Dashboard, Live, Players list, Societies, Houses, Item Finder, Job Grades, Analytics | ✅ | ✅ | ✅ | ✅ |
| Player profile, Society detail | ❌ | ✅ | ✅ | ✅ |
| Vehicles, Character Kill form, Swap form, Job reset, Add licence, Audit Log | ❌ | ❌ | ✅ | ✅ |
| Licenses page, Economy, Business, Anomalies, Staff Activity, Webhooks, Settings, Permissions | ❌ | ❌ | ❌ | ✅ |

Pages in the last row can be granted to other roles on the Permissions page.

### Default action permissions

| Action | Minimum role |
| ------ | ------------ |
| `edit_user_data` | Admin |
| `edit_user_money` | Admin |
| `delete_job` | Admin |
| `manage_vehicles` | Admin |
| `manage_licenses` | Moderator |
| `character_kill` | Superadmin |
| `swap_players` | Superadmin |
| `create_staff` | Superadmin |
| `remove_staff` | Superadmin |
| `change_password` | Moderator |
| `view_settings` | Superadmin |
| `manage_permissions` | Superadmin |
| `export_players` | Admin |
| `export_audit` | Admin |
| `purge_audit` | Superadmin |
| `manage_webhooks` | Superadmin |

Staff notes and tags on player profiles require Moderator or higher.

---

## Webhook System

### Formats

| Format | Detected by | Payload |
| ------ | ----------- | ------- |
| **Discord** | URL contains `discord.com/api/webhooks` | Embed with colour, fields and timestamp |
| **Slack** | URL contains `hooks.slack.com` | Attachment with colour and fields |
| **Generic JSON** | any other URL | `event`, `data` and timestamp |

### Events (13)

| Event | Trigger |
| ----- | ------- |
| `user.money_changed` | Account balance edited |
| `user.data_changed` | Player data edited |
| `user.license_added` | Licence granted |
| `user.license_swapped` | Swap performed |
| `user.character_killed` | Character kill performed |
| `user.job_reset` | Job reset to the default job |
| `vehicle.deleted` | Vehicle(s) deleted |
| `vehicle.sent_garage` | Vehicle sent to garage |
| `staff.created` | Staff account created |
| `staff.removed` | Staff account deleted |
| `staff.password_changed` | Staff password changed |
| `panel.login` | Successful login |
| `panel.login_failed` | Failed login |

### Setup

1. Open **Webhooks** in the sidebar
2. Create a webhook with a name, URL and the events to send
3. Use **Test** to send a test payload

Each delivery is logged in `panel_webhook_logs` (event, payload, HTTP status, response) and listed on the Webhooks page.

### Legacy Discord webhook

`$WEBHOOK_URL` in `config.php` still receives a plain-text message for actions logged through the legacy audit function. Prefer the Webhooks page.

---

## Audit Log

Staff actions are stored in `panel_audit_log` and shown on the **Audit Log** page.

**Recorded:** timestamp, username, role, action, category, target, details, IP address, user agent.

- 7 filters: user, category, action, target, IP, date from, date to
- 50 entries per page
- Stats: today, this week, active users, failed logins (24 h)
- CSV export of the filtered results
- Purge of entries older than N days (minimum 7); the purge form is shown to Superadmins

---

## Intelligence Suite

### Economy (`economy.php`)

- Totals per account, average and maximum wealth (wealth = bank + cash + black money; the extra currency is excluded)
- Top richest players, money by job, wealth brackets
- Players above the wealth multiplier, cash-heavy players
- Charts

### Business Analytics (`business-analytics.php`)

- Headcount per job, society funds
- Active / semi-active / inactive employees per job (by last seen)
- Abandoned businesses, unemployed count, grade distribution

### Anomaly Detection (`anomalies.php`)

| Alert | Severity |
| ----- | -------- |
| **Extreme wealth** (above mean + N standard deviations) | Critical, high or medium, by multiple of the average |
| **Negative balance** | Low |

- Alerts are generated when the page loads and de-duplicated within the dedup window
- Filter by severity and type; dismiss alerts
- Stored in `panel_anomaly_alerts`

### Staff Activity (`staff-activity.php`)

- Total actions, active staff, average per day
- Daily trend, category breakdown, hourly heatmap
- Staff ranking and most common actions
- Range: 7, 14, 30, 60 or 90 days

---

## Customization

### Translations

UI strings are in `translations.php` as `$T_*` variables:

```php
$T_PAGE_HOME = 'Home';
$T_TOTAL_CARS = 'Total Cars';
```

The panel ships in English.

### Theme

Pick the accent colour and logo in **Settings → Appearance**. For deeper changes, edit the CSS variables at the top of `style.css`:

```css
:root {
    --bg: #0e1116;          /* page background */
    --surface: #151a21;     /* cards */
    --border: #252c36;
    --text: #e7eaef;
    --text-2: #a7afbc;      /* secondary text */
    --accent: #c5f24a;      /* overridden by Settings → Appearance */
}
```

### Menu

The sidebar groups and entries are defined in `panel_menu()` in `includes/layout.php`.

---

## Database Reference

### Panel tables

Created in your FiveM database with `CREATE TABLE IF NOT EXISTS`. Running the installer or `setup/schema.sql` again is safe.

| Table | Purpose |
| ----- | ------- |
| `superadmins` | Staff accounts (bcrypt passwords, role) |
| `analytics_money` | Money snapshots from the cron job |
| `panel_audit_log` | Staff action trail |
| `panel_webhooks` | Webhook endpoints and events |
| `panel_webhook_logs` | Webhook deliveries |
| `panel_roles` | Roles and their permissions (JSON) |
| `panel_player_notes` | Staff notes and tags on players |
| `panel_anomaly_alerts` | Anomaly alerts |
| `panel_settings` | Settings key/value store |

### Game tables used by the panel

The panel never creates, drops or alters these tables. It reads them and, for the operations listed, writes rows.

| Purpose | ESX | QBCore / Qbox | Operations |
| ------- | --- | ------------- | ---------- |
| Players | `users` | `players` (`charinfo`, `job`, `gang`, `money`, `metadata` JSON) | Read / update / delete (CK) / swap |
| Licences | `user_licenses` | `players.metadata.licences` | Read / write / delete |
| Vehicles | `owned_vehicles` | `player_vehicles` | Read / update / delete |
| Skins / outfits | `users.skin`, `datastore_data` | `playerskins`, `player_outfits` | Read / update skin / delete (CK) |
| Job groups | — | `player_groups` (Qbox) | Write on job change / delete (CK) |
| Job grades | `job_grades`, `jobs` | — (`shared/jobs.lua`) | Read |
| Business funds | `addon_account`, `addon_account_data` | `management_funds`, `bank_accounts`, `bank_accounts_new` | Read |
| Billing | `billing` | `phone_invoices` | Delete (CK) / swap (ESX) |
| Society items | `addon_inventory_items`, `datastore_data` | `stashitems`, `trunkitems`, `gloveboxitems`, `inventories` | Read / delete and swap (ESX) |
| ox_inventory | `ox_inventory` | `ox_inventory` | Read / delete (CK) / swap (ESX) |
| Housing | loaf_housing, `owned_properties`, `properties` | `properties`, `player_houses` | Read / delete (CK) / swap (ESX) |
| Phone | `phone_phones` | `phone_phones` | Read / delete (CK, ESX) |
| Items | `items` | — | Read |
| Vehicle names | `vehicles` (esx_vehicleshop) | — | Read |
| Qbox users | — | `users` (Qbox) | Read (swap) |
| Premium | configured table | configured table | Read / delete (CK, ESX) |

---

## File Structure

```
xex-panel/
├── config.php                      # Created by the installer (values only)
├── .htaccess                       # Apache protection rules and headers
├── index.php                       # Login
├── export-players.php              # Player list CSV export
├── home.php                        # Dashboard
├── live.php                        # Live server
├── users.php                       # Player list
├── view-user.php                   # Player profile
├── licenses.php                    # Grant licences
├── makelicenses.php                # Licence grant handler
├── delete-job.php                  # Job reset handler
├── ck_user.php                     # Character kill form
├── makeck.php                      # Character kill handler
├── swaps.php                       # Swap form
├── societies.php                   # Society list
├── view-societies.php              # Society detail
├── grades.php                      # Job grades
├── houses.php                      # Housing
├── vehicles.php                    # Vehicles
├── itemsFinder.php                 # Item catalogue and search form
├── search-items.php                # Item search results
├── analytics.php                   # Money over time
├── economy.php                     # Economy
├── business-analytics.php          # Business analytics
├── anomalies.php                   # Anomaly detection
├── staff-activity.php              # Staff activity
├── audit.php                       # Audit log
├── webhooks.php                    # Webhooks
├── settings.php                    # Settings (8 tabs)
├── permissions.php                 # Roles and permissions
├── profile.php                     # Own profile and password
├── logout.php
├── translations.php                # UI strings
├── style.css                       # Styles (CSS variables)
├── main.js                         # Sidebar, quick search, tables
├── favicon.ico
│
├── controllers/                    # Form handlers (POST only, except quick search)
│   ├── authenticate.php            # Login
│   ├── createuser.php              # Create staff account
│   ├── data-controller.php         # Player data edits
│   ├── editpass-controller.php     # Password change
│   ├── money-controller.php        # Account edits
│   ├── notes-controller.php        # Player notes and tags
│   ├── remove-license-controller.php
│   ├── remove-staff-controller.php
│   ├── remove-veh-controller.php   # Delete vehicle(s)
│   ├── search-global.php           # Quick search (GET, JSON)
│   ├── search-veh-controller.php   # Vehicle search by plate
│   ├── send-veh-controller.php     # Send vehicle to garage
│   ├── settings-controller.php     # Settings, logo and item uploads
│   └── swap-controller.php         # Swap
│
├── includes/
│   ├── bootstrap.php               # Loaded by config.php: roles, default access, helpers
│   ├── security.php                # Sessions, headers, CSRF, DB connection, CK and swap
│   ├── framework.php               # Framework adapter and schema detection
│   ├── fw_players.php              # Adapter: per-player helpers
│   ├── fw_world.php                # Adapter: economy, housing, vehicles, items, live API
│   ├── settings_helper.php         # Settings store, appearance helpers
│   ├── permissions.php             # Granular permissions engine
│   ├── webhooks.php                # Webhook delivery
│   ├── layout.php                  # Menu definition and page titles
│   ├── header.php                  # <head> assets (CDN with SRI)
│   ├── sidebar.php                 # Sidebar shell
│   ├── topbar.php                  # Top bar shell
│   ├── menu.php                    # Sidebar links filtered by access and detected data
│   └── chart.php                   # Chart.js loader and defaults
│
├── install/
│   └── index.php                   # Installer (6 steps)
│
├── setup/
│   ├── schema.sql                  # Panel tables
│   └── cronjob_money_analytics.php # Analytics snapshots
│
├── assets/
│   ├── data/                       # items.json (created by the item catalogue upload)
│   └── img/                        # custom-logo.* (created by the logo upload)
│
└── cache/                          # Runtime files
    ├── schema.json                 # Detected tables (~2 min)
    ├── users_<framework>.json      # Player list cache
    ├── cacheAnalytics.txt          # Analytics cache
    └── login_attempts.json         # Login rate limiting
```

---

## Troubleshooting

| Issue | Cause | Solution |
| ----- | ----- | -------- |
| **"Database connection error"** | Wrong credentials, wrong port or database down | Check `$DDBB_HOST` (use `host:port` for a non-default port), `$DDBB_USER`, `$DDBB_PASSWORD`, `$DATABASE_NAME`. |
| **Blank page** | PHP error hidden in production mode | Set `$MODE = "development"` temporarily and check the PHP error log. |
| **Missing panel table errors** | Panel tables not created | Run the installer or import `setup/schema.sql`. |
| **"Invalid security token"** | Session expired or form opened in another session | Log in again; increase the session timeout in Settings if needed. |
| **A menu entry or page section is missing** | The resource was not detected | Check **Settings → System → Detected resources**, click **Rescan**, or set the table name in `$DB_TABLES`. |
| **Wrong framework** | Detection picked another layout | Set `$FRAMEWORK` in `config.php` (`esx`, `qbcore`, `qbox`, `custom`). |
| **Items show internal names** | No item catalogue | Upload `ox_inventory/data/items.lua` or `qb-core/shared/items.lua` in **Settings → Game Config → Item catalogue**. |
| **Logo upload fails** | File too large or folder not writable | Use PNG/JPG/WebP up to 512 KB; make `assets/img/` writable. |
| **Analytics page empty** | No snapshots yet | Set up the [cron job](#analytics-cron-job-optional). |
| **Live page empty** | API URL missing, wrong or server not reachable | Set **Settings → General → FiveM API URL**. The page shows the error it received (connection, HTTP status or invalid JSON). |
| **Quick search returns nothing (Apache)** | Old `.htaccess` blocks GET to controllers | Use the 2.2 `.htaccess`, which allows GET to `controllers/search-global.php`. |
| **Redirect to HTTPS on a server without a certificate** | Old `.htaccess` | The 2.2 `.htaccess` skips the redirect for `localhost`, `127.0.0.1` and `[::1]`. For other hosts, install a certificate or remove the redirect block. |
| **Installer blocked** | Already installed | Delete `install/installed.lock`. |
| **Permissions not saving** | Missing DB rights | The MySQL user needs `INSERT`/`UPDATE` on `panel_roles`. |

### Log files

- **PHP errors:** your PHP error log (XAMPP: `C:\xampp\php\logs\php_error_log`)
- **Webhook deliveries:** Webhooks page or `panel_webhook_logs`
- **Login attempts:** `cache/login_attempts.json`
- **Staff actions:** Audit Log page or `panel_audit_log`

---

## Security

### Authentication & sessions

- Passwords hashed with `password_hash()` (`PASSWORD_DEFAULT`), rehashed on login when needed
- Password policy for new accounts and changes: 8+ characters with uppercase, lowercase and a number
- Accounts flagged with `force_password_change` must change their password before using the panel
- Session ID regenerated on login
- Session cookie re-issued with `HttpOnly`, `SameSite=Strict`, and `Secure` when the request is HTTPS
- Session timeout (default 1 hour)
- Login rate limiting per IP, stored on disk
- The login error does not reveal whether a username exists

### Request protection

- CSRF tokens on forms (including the item search), rotated after each successful check
- State-changing operations use POST
- PDO prepared statements with emulation disabled; values such as thresholds and job names are bound as parameters
- Table and column names from configuration are validated before use
- Output escaped with `e()` / `attr()`; data passed to JavaScript is JSON-encoded

### HTTP headers

Sent by `includes/security.php` (and by `.htaccess` on Apache):

- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `X-XSS-Protection: 1; mode=block`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
- `Strict-Transport-Security` (HTTPS requests only)

### Front-end dependencies

Loaded from CDNs with Subresource Integrity hashes: jQuery 3.7.1, DataTables core 1.13.7, Font Awesome 6.5.1, Chart.js 4.4.1. Fonts come from Google Fonts.

### File protection

`.htaccess` (Apache):
- Redirects HTTP to HTTPS, except for `localhost`, `127.0.0.1` and `[::1]`
- Blocks non-POST requests to `controllers/*.php`, except `controllers/search-global.php` (quick search)
- Blocks `includes/*.php`, `setup/` and `cache/`
- Denies `.sql`, `.log`, `.txt`, `.md`, `.json`, `.bak`, `.old`, `.orig`, `.save` files and `config.php`
- Disables directory listing

On Nginx, add equivalent rules to your server block.

### Production checklist

1. Serve the panel over HTTPS
2. Restrict access (firewall, VPN or IP allow-list)
3. Keep `$MODE = "production"`
4. Delete or block `install/` after installation
5. Review the audit log regularly
6. Keep PHP and MySQL/MariaDB updated
7. Back up the database, including panel tables

---

## Changelog

### v2.2.0

**New**
- Player list export to CSV
- Read-only demo mode (`$DEMO_MODE`)
- Analytics shows one chart per account with its own scale and the 30-day change
- `export_audit`, `purge_audit` and `manage_webhooks` are now enforced and can be granted to custom roles

**Frameworks**
- ESX Legacy, QBCore and Qbox are supported natively through a new adapter (`includes/framework.php`, `includes/fw_players.php`, `includes/fw_world.php`)
- QBCore/Qbox JSON columns (`charinfo`, `job`, `gang`, `money`, `metadata.licences`) are read with `JSON_EXTRACT` and written with `JSON_SET`
- Qbox `player_groups` is kept in sync on job changes
- The installer detects ESX, QBCore and Qbox and has a Qbox option

**Configuration**
- `config.php` holds only values and ends with `require_once includes/bootstrap.php`; defaults per framework moved to `includes/framework.php`
- `$DB_TABLES` is now for overrides only
- The database host can include a port (`127.0.0.1:3307`)
- Configs generated by 2.1 keep working

**Resources and data**
- Optional resources are detected automatically; their pages and menu entries are hidden when absent
- New **Settings → System → Detected resources** with **Rescan**
- Housing: loaf_housing, esx_property, ps-housing, qbx_properties, qb-houses
- Business funds: ESX addon accounts, qb-management, qb-banking, Renewed-Banking
- Billing: ESX `billing`, qb-phone `phone_invoices`
- Accounts follow the framework; QBCore/Qbox crypto is shown as the extra currency. A custom currency can be set with `account_coin`
- Item catalogue upload (`items.lua` from ox_inventory or qb-core, or JSON) replaces the bundled item list
- Character kill and swap are framework-aware; QBCore/Qbox swap moves a character to another Rockstar license; ESX swap refuses swapping an identifier onto itself
- Send to garage and delete by plate work on all frameworks; delete job vehicles is ESX only
- The analytics cron records only the accounts that exist

**Interface**
- New dark UI with grouped, collapsible sidebar and top bar
- Quick search (`Ctrl+K` / `Cmd+K`) for players, plates and pages, case-insensitive on every framework
- **Settings → Appearance**: accent colour and logo upload
- Bootstrap 3 removed; DataTables core 1.13.7 and Chart.js 4.4.1 pinned with SRI
- Shared page shell: `includes/sidebar.php`, `includes/topbar.php`, `includes/layout.php`, `includes/header.php`

**Security**
- Hardened session cookie flags are now actually applied (`HttpOnly`, `SameSite=Strict`, `Secure` on HTTPS)
- Login no longer reveals whether a username exists
- `.htaccess` no longer forces HTTPS on localhost and allows GET to the quick search endpoint only
- CSRF check added to the item search
- Output-escaping fixes on several pages
- SQL thresholds bound as parameters

**Removed**
- Bundled images in `assets/img/` and the bundled item list in `assets/data/`

### v2.1.0

- **Intelligence Suite** — Economy Dashboard, Business Analytics, Anomaly Detection, Staff Activity
- **Dynamic Settings** — Full in-panel configuration (no file editing for most settings)
- **Granular Permissions** — DB-backed per-role permissions engine with custom role creation
- **Webhook System** — Multi-endpoint webhooks with Discord, Slack, and generic JSON support (13 events)
- **Player Notes** — Staff-to-staff notes on player profiles
- **Advanced Audit Log** — 7 filters, CSV export, stats cards, auto-purge
- **Wealth Brackets** — Configurable wealth distribution ranges
- **Staff Activity Tracking** — Heatmaps, trend charts, ranking, category breakdown

### v2.0.0

- **Multi-framework support** — ESX Legacy, QBCore, and custom framework table mapping
- **Role-based access control** — 4 built-in roles with configurable page and action permissions
- **CSRF protection** — All forms and state-changing requests
- **XSS protection** — Output escaping on all user-generated data
- **Secure caching** — JSON-based cache replacing insecure PHP serialization
- **Rate limiting** — Login attempt throttling with configurable lockout
- **Modernized UI** — Dark theme with Inter font family, CSS variables, responsive design
- **Updated dependencies** — jQuery 3.7, DataTables 1.13, Font Awesome 6.5
- **Security hardening** — Removed exposed password hashes, POST-only controllers, self-deletion protection, debug cleanup

---


## License

This is a commercial product. Unauthorized redistribution, resale, or modification for resale is prohibited.

© 2024–2026 XeX Panel. All rights reserved.
