first commit

This commit is contained in:
2026-07-16 09:35:32 +02:00
commit dd0746e2e6
179 changed files with 25679 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
-- d4rk_tablet — Bridge Client
-- Tablet (Web-iframe) öffnen, Auth-Token an die NUI übergeben, Waypoint setzen.
local isOpen = false
local function closeTablet()
if not isOpen then return end
isOpen = false
SetNuiFocus(false, false)
SendNUIMessage({ action = 'close' })
end
local function openTablet()
if isOpen then return end
-- Token vom Server holen (Server-zu-Server zum Backend)
local auth = lib.callback.await('d4rk_tablet:getAuth', false)
if not auth or not auth.token then
lib.notify({ title = 'MDT', description = 'Kein Zugriff (kein Behörden-Job?).', type = 'error' })
return
end
isOpen = true
SetNuiFocus(true, true)
SendNUIMessage({
action = 'open',
webUrl = Config.WebUrl,
token = auth.token,
user = auth.user,
})
end
RegisterCommand(Config.OpenCommand, openTablet, false)
RegisterKeyMapping(Config.OpenCommand, 'MDT öffnen', 'keyboard', '')
-- Aus der Web-App (via NUI-Shell) kommende Aktionen
RegisterNUICallback('close', function(_, cb)
closeTablet()
cb({ ok = true })
end)
RegisterNUICallback('setWaypoint', function(data, cb)
if data and data.x and data.y then
SetNewWaypoint(data.x + 0.0, data.y + 0.0)
lib.notify({ title = 'MDT', description = 'Wegpunkt gesetzt.', type = 'success' })
end
cb({ ok = true })
end)
-- ESC schließt das Tablet
CreateThread(function()
while true do
if isOpen then
if IsControlJustReleased(0, 322) then -- ESC
closeTablet()
end
Wait(0)
else
Wait(250)
end
end
end)
AddEventHandler('onResourceStop', function(resource)
if resource == GetCurrentResourceName() and isOpen then
SetNuiFocus(false, false)
end
end)
+29
View File
@@ -0,0 +1,29 @@
Config = {}
-- Basis-URL des MDT-Backends (Server-zu-Server)
Config.BackendUrl = GetConvar('mdt:backendUrl', 'http://127.0.0.1:3000')
-- Shared Secret zwischen Bridge (Server) und Backend.
-- MUSS BRIDGE_HMAC_SECRET in der Backend-.env entsprechen. In server.cfg setzen:
-- set mdt:bridgeSecret "dein-langes-geheimnis"
Config.Secret = GetConvar('mdt:bridgeSecret', 'dev-insecure-bridge-secret')
-- Gehostete Web-App, die im NUI-iframe geladen wird
Config.WebUrl = GetConvar('mdt:webUrl', 'http://localhost:5190')
-- Command zum Öffnen des Tablets
Config.OpenCommand = GetConvar('mdt:openCommand', 'mdt')
-- Jobs, die das MDT öffnen dürfen → Basis-Department (police/ems/fire)
Config.JobDepartments = {
police = 'police',
bcso = 'police',
sheriff = 'police',
doj = 'police',
ambulance = 'ems',
ems = 'ems',
fire = 'fire',
}
-- Intervall (ms) für den server-seitigen Positions-Push (nur im Dienst)
Config.PositionInterval = 5000
+36
View File
@@ -0,0 +1,36 @@
fx_version 'cerulean'
game 'gta5'
author 'D4rkst3r'
description 'd4rk_tablet — FiveM Bridge (Token, NUI-Öffner, Live-Sync zum MDT-Backend)'
version '1.0.0'
lua54 'yes'
dependencies {
'qbx_core',
'ox_lib',
}
shared_scripts {
'@ox_lib/init.lua',
'config.lua',
}
client_scripts {
'client/main.lua',
}
server_scripts {
'server/main.lua',
}
ui_page 'nui/index.html'
files {
'nui/index.html',
'nui/app.js',
'nui/style.css',
}
-- HINWEIS: Die MDT-Logik lebt im Backend/Web. Diese Resource ist bewusst minimal:
-- Token holen · NUI (Web-iframe) öffnen · Positionen/Dienst pushen · Waypoint setzen.
+55
View File
@@ -0,0 +1,55 @@
// d4rk_tablet — NUI-Shell
// Relayt zwischen FiveM (SendNUIMessage / NUI-Callbacks) und der Web-App im iframe (postMessage).
const wrap = document.getElementById('wrap');
const iframe = document.getElementById('mdt');
let pendingAuth = null; // { token, user } bis das iframe "mdt:ready" meldet
function resourceName() {
return typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'd4rk_tablet';
}
function nui(endpoint, body) {
return fetch(`https://${resourceName()}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {}),
}).catch(() => {});
}
function sendAuth() {
if (pendingAuth && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{ action: 'auth', token: pendingAuth.token, user: pendingAuth.user },
'*',
);
}
}
// Token nachschieben, sobald das iframe fertig geladen ist
iframe.addEventListener('load', sendAuth);
window.addEventListener('message', (event) => {
const data = event.data || {};
// 1) Nachrichten aus der Web-App (iframe)
if (event.source === iframe.contentWindow) {
if (data.action === 'mdt:ready') sendAuth();
else if (data.action === 'mdt:close') nui('close');
else if (data.action === 'officer:setWaypoint') nui('setWaypoint', { x: data.x, y: data.y });
return;
}
// 2) Nachrichten aus FiveM (SendNUIMessage)
if (data.action === 'open') {
pendingAuth = { token: data.token, user: data.user };
const target = data.webUrl;
if (iframe.src !== target) {
iframe.src = target; // 'load' → sendAuth()
} else {
sendAuth(); // bereits geladen (Reopen)
}
wrap.classList.remove('hidden');
} else if (data.action === 'close') {
wrap.classList.add('hidden');
}
});
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>d4rk_tablet</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Dünne Shell: bettet die gehostete MDT-Web-App als iframe ein und relayed Nachrichten. -->
<div id="wrap" class="hidden">
<iframe id="mdt" title="MDT" allow="clipboard-read; clipboard-write"></iframe>
</div>
<script src="app.js"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: transparent;
}
#wrap {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
#wrap.hidden {
display: none;
}
#mdt {
width: 100%;
height: 100%;
border: 0;
}
+120
View File
@@ -0,0 +1,120 @@
-- d4rk_tablet — Bridge Server
-- Token holen (Shared-Secret), Dienst + Positionen an /bridge/* pushen, Dispatch-Export.
local onDuty = {} -- [src] = { citizenid, name, callsign, department }
--- HTTP POST an das Backend (mit Shared-Secret-Header).
local function post(path, body, cb)
PerformHttpRequest(Config.BackendUrl .. path, function(status, res)
if cb then cb(status, res) end
end, 'POST', json.encode(body), {
['Content-Type'] = 'application/json',
['x-bridge-secret'] = Config.Secret,
})
end
--- Baut den Auth-Body aus den QBox-Charakterdaten.
local function authBody(player)
local pd = player.PlayerData
local job = pd.job or {}
local grade = job.grade or {}
return {
license = pd.license,
citizenid = pd.citizenid,
name = ('%s %s'):format(pd.charinfo.firstname, pd.charinfo.lastname),
job = job.name,
gradeLevel = grade.level or 0,
callsign = pd.metadata and pd.metadata.callsign or nil,
}
end
-- ── Token holen (Client öffnet Tablet) ──
lib.callback.register('d4rk_tablet:getAuth', function(source)
local player = exports.qbx_core:GetPlayer(source)
if not player then return nil end
if not Config.JobDepartments[player.PlayerData.job.name] then return nil end -- kein Behörden-Job
local p = promise.new()
post('/auth/fivem', authBody(player), function(status, res)
if status == 200 and res then
local ok, data = pcall(json.decode, res)
if ok and data and data.token then
p:resolve({ token = data.token, user = data.user })
return
end
end
p:resolve(nil)
end)
return Citizen.Await(p)
end)
-- ── Dienststatus ──
local function setDuty(src, isOn)
local player = exports.qbx_core:GetPlayer(src)
if not player then return end
local jobName = player.PlayerData.job.name
local department = Config.JobDepartments[jobName]
if not department then return end
if isOn then
onDuty[src] = {
citizenid = player.PlayerData.citizenid,
name = ('%s %s'):format(player.PlayerData.charinfo.firstname, player.PlayerData.charinfo.lastname),
callsign = player.PlayerData.metadata and player.PlayerData.metadata.callsign or nil,
department = department,
}
end
local o = onDuty[src] or {
citizenid = player.PlayerData.citizenid,
name = ('%s %s'):format(player.PlayerData.charinfo.firstname, player.PlayerData.charinfo.lastname),
department = department,
}
post('/bridge/officer/duty', {
citizenid = o.citizenid,
name = o.name,
callsign = o.callsign,
department = o.department,
onDuty = isOn,
})
if not isOn then onDuty[src] = nil end
end
-- QBox-Dienstwechsel (best effort) + Fallback-Command
RegisterNetEvent('QBCore:Server:SetDuty', function(src, isOn)
setDuty(src, isOn == true)
end)
lib.addCommand('mdtduty', { help = 'MDT: Dienststatus umschalten' }, function(source)
setDuty(source, onDuty[source] == nil)
end)
AddEventHandler('playerDropped', function()
local src = source
if onDuty[src] then setDuty(src, false) end
end)
-- ── Server-seitiger Positions-Push (vertraut Client nicht) ──
CreateThread(function()
while true do
Wait(Config.PositionInterval)
for src, info in pairs(onDuty) do
local ped = GetPlayerPed(src)
if ped and ped ~= 0 then
local c = GetEntityCoords(ped)
post('/bridge/officer/position', {
citizenid = info.citizenid,
coords = { x = c.x, y = c.y, z = c.z },
})
end
end
end
end)
-- ── Positionsziel an einen Officer senden (von der Leitstelle via Web → Socket → hier) ──
RegisterNetEvent('d4rk_tablet:relayWaypoint', function() end) -- Platzhalter M8 (Socket→Bridge)
-- ── Dispatch-Export für andere Resourcen ──
-- exports.d4rk_tablet:createDispatch({ code='10-90', title='Banküberfall', department='police', priority='high', location='...', coords={x=,y=,z=} })
exports('createDispatch', function(data)
post('/bridge/dispatch', data)
end)