- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
228 lines
8.1 KiB
Lua
228 lines
8.1 KiB
Lua
-- =====================================================================
|
|
-- aether-police — Kern
|
|
--
|
|
-- Der Kern kennt keine Fachlogik. Er stellt bereit:
|
|
-- * eine Modul-Registry (Plugin-System)
|
|
-- * einen Ereignisbus zwischen den Modulen
|
|
-- * ein Handler-Register, an das die Oberfläche Anfragen schickt
|
|
--
|
|
-- Fachmodule registrieren sich selbst und können jederzeit ergänzt
|
|
-- werden, ohne dass der Kern angefasst werden muss.
|
|
-- =====================================================================
|
|
|
|
Polizei = Polizei or {}
|
|
Kern = {}
|
|
|
|
-- Registrierte Fachmodule: [id] = { id, name, icon, recht, reihenfolge, aktiv }
|
|
local module = {}
|
|
-- Handler, die die Oberfläche aufrufen darf: [modulId][aktion] = funktion
|
|
local handler = {}
|
|
-- Abonnenten des Ereignisbusses: [ereignis] = { funktion, ... }
|
|
local abonnenten = {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Modul-Registry
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Registriert ein Fachmodul.
|
|
--- @param beschreibung table {
|
|
--- id eindeutige Kennung
|
|
--- name Anzeigename in der Seitenleiste
|
|
--- icon Symbol
|
|
--- recht Recht, das zum Öffnen nötig ist (nil = für alle)
|
|
--- reihenfolge Sortierung in der Seitenleiste
|
|
--- aktiv Standardmäßig aktiv?
|
|
--- }
|
|
function Kern.RegistriereModul(beschreibung)
|
|
if type(beschreibung) ~= 'table' or not beschreibung.id then
|
|
Aether.Fehler('Modul ohne Kennung kann nicht registriert werden.')
|
|
return false
|
|
end
|
|
|
|
if module[beschreibung.id] then
|
|
Aether.Fehler(('Modul "%s" ist bereits registriert.'):format(beschreibung.id))
|
|
return false
|
|
end
|
|
|
|
module[beschreibung.id] = {
|
|
id = beschreibung.id,
|
|
name = beschreibung.name or beschreibung.id,
|
|
icon = beschreibung.icon or '▪',
|
|
recht = beschreibung.recht,
|
|
reihenfolge = beschreibung.reihenfolge or 100,
|
|
aktiv = beschreibung.aktiv ~= false,
|
|
beschreibung = beschreibung.beschreibung,
|
|
}
|
|
|
|
handler[beschreibung.id] = handler[beschreibung.id] or {}
|
|
Aether.Debug(('Polizei-Modul registriert: %s'):format(beschreibung.id))
|
|
return true
|
|
end
|
|
|
|
--- Registriert einen Handler, den die Oberfläche aufrufen kann.
|
|
--- @param modulId string Modul
|
|
--- @param aktion string Name der Aktion
|
|
--- @param recht string|nil Erforderliches Recht (nil = nur Modulrecht)
|
|
--- @param funktion function fun(quelle, beamter, daten): ergebnis
|
|
function Kern.RegistriereAktion(modulId, aktion, recht, funktion)
|
|
handler[modulId] = handler[modulId] or {}
|
|
handler[modulId][aktion] = { recht = recht, funktion = funktion }
|
|
end
|
|
|
|
--- Prüft das Öffnungsrecht eines Moduls.
|
|
--- `recht` darf eine Kennung oder eine Liste sein — bei einer Liste
|
|
--- genügt EIN Recht daraus. Das brauchen Module, die mehrere Bereiche
|
|
--- mit je eigenem Recht unter einem Dach zusammenfassen.
|
|
--- @param beamter table Beamtendaten
|
|
--- @param recht string|table|nil Erforderliches Recht
|
|
--- @return boolean
|
|
function Kern.DarfOeffnen(beamter, recht)
|
|
if not recht then return true end
|
|
|
|
if type(recht) == 'table' then
|
|
for _, einzeln in ipairs(recht) do
|
|
if Rechte.Hat(beamter, einzeln) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
return Rechte.Hat(beamter, recht)
|
|
end
|
|
|
|
--- Liefert alle Module, die ein Beamter sehen darf
|
|
--- @param beamter table Beamtendaten (mit Rechten)
|
|
--- @return table Liste
|
|
function Kern.SichtbareModule(beamter)
|
|
local liste = {}
|
|
for _, modul in pairs(module) do
|
|
if modul.aktiv and Kern.DarfOeffnen(beamter, modul.recht) then
|
|
liste[#liste + 1] = {
|
|
id = modul.id, name = modul.name, icon = modul.icon,
|
|
reihenfolge = modul.reihenfolge, beschreibung = modul.beschreibung,
|
|
}
|
|
end
|
|
end
|
|
table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end)
|
|
return liste
|
|
end
|
|
|
|
--- Liefert alle registrierten Module, auch die abgeschalteten.
|
|
--- Nur für die Administration gedacht.
|
|
--- @return table Liste
|
|
function Kern.AlleModule()
|
|
local liste = {}
|
|
for _, modul in pairs(module) do
|
|
-- Wie viele Aktionen hängen an diesem Modul?
|
|
local aktionen = 0
|
|
for _ in pairs(handler[modul.id] or {}) do aktionen = aktionen + 1 end
|
|
|
|
liste[#liste + 1] = {
|
|
id = modul.id, name = modul.name, icon = modul.icon,
|
|
recht = modul.recht, reihenfolge = modul.reihenfolge,
|
|
beschreibung = modul.beschreibung,
|
|
aktiv = modul.aktiv, aktionen = aktionen,
|
|
}
|
|
end
|
|
table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end)
|
|
return liste
|
|
end
|
|
|
|
--- Gibt es dieses Modul?
|
|
--- @param modulId string Modul
|
|
--- @return boolean
|
|
function Kern.KenntModul(modulId)
|
|
return module[modulId] ~= nil
|
|
end
|
|
|
|
--- Schaltet ein Modul an oder aus (Administration)
|
|
--- @param modulId string Modul
|
|
--- @param aktiv boolean Neuer Zustand
|
|
function Kern.SetzeModulAktiv(modulId, aktiv)
|
|
if module[modulId] then
|
|
module[modulId].aktiv = aktiv and true or false
|
|
end
|
|
end
|
|
|
|
--- Führt eine Aktion aus — inklusive Rechteprüfung.
|
|
--- Wird von der Oberfläche über ein einziges Netz-Event aufgerufen.
|
|
--- @param quelle number Server-ID
|
|
--- @param beamter table Beamtendaten
|
|
--- @param modulId string Modul
|
|
--- @param aktion string Aktion
|
|
--- @param daten table Nutzdaten
|
|
--- @return any Ergebnis, string|nil Fehler
|
|
function Kern.FuehreAus(quelle, beamter, modulId, aktion, daten)
|
|
local modul = module[modulId]
|
|
if not modul or not modul.aktiv then
|
|
return nil, 'Dieses Modul ist nicht verfügbar.'
|
|
end
|
|
|
|
-- Recht auf das Modul selbst
|
|
if not Kern.DarfOeffnen(beamter, modul.recht) then
|
|
return nil, 'Dir fehlt die Berechtigung für diesen Bereich.'
|
|
end
|
|
|
|
local eintrag = handler[modulId] and handler[modulId][aktion]
|
|
if not eintrag then
|
|
return nil, 'Unbekannte Aktion.'
|
|
end
|
|
|
|
-- Zusätzliches Recht auf die einzelne Aktion
|
|
if eintrag.recht and not Rechte.Hat(beamter, eintrag.recht) then
|
|
return nil, 'Dir fehlt die Berechtigung für diese Aktion.'
|
|
end
|
|
|
|
local erfolg, ergebnis, fehler = pcall(eintrag.funktion, quelle, beamter, daten or {})
|
|
if not erfolg then
|
|
Aether.Fehler(('Fehler in %s/%s: %s'):format(modulId, aktion, tostring(ergebnis)))
|
|
return nil, 'Interner Fehler im Modul.'
|
|
end
|
|
|
|
return ergebnis, fehler
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Ereignisbus zwischen den Modulen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Abonniert ein internes Ereignis
|
|
--- @param ereignis string Name
|
|
--- @param funktion function Rückruf
|
|
function Kern.Abonniere(ereignis, funktion)
|
|
abonnenten[ereignis] = abonnenten[ereignis] or {}
|
|
table.insert(abonnenten[ereignis], funktion)
|
|
end
|
|
|
|
--- Löst ein internes Ereignis aus.
|
|
--- Fehler eines Abonnenten dürfen die anderen nicht mitreißen.
|
|
--- @param ereignis string Name
|
|
--- @param ... any Nutzdaten
|
|
function Kern.Loese(ereignis, ...)
|
|
for _, funktion in ipairs(abonnenten[ereignis] or {}) do
|
|
local erfolg, fehler = pcall(funktion, ...)
|
|
if not erfolg then
|
|
Aether.Fehler(('Abonnent von "%s" schlug fehl: %s'):format(ereignis, tostring(fehler)))
|
|
end
|
|
end
|
|
|
|
-- Zusätzlich nach außen geben, damit fremde Ressourcen andocken können
|
|
TriggerEvent('aether:police:' .. ereignis, ...)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen für Fachmodule
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Erzeugt eine fortlaufende Kennung, z. B. AZ-2026-0042
|
|
--- @param praefix string Vorsilbe
|
|
--- @param tabelle string Tabellenname
|
|
--- @param spalte string Spalte mit der Kennung
|
|
--- @return string
|
|
function Kern.ErzeugeKennung(praefix, tabelle, spalte)
|
|
local jahr = os.date('%Y')
|
|
local anzahl = MySQL.scalar.await(
|
|
('SELECT COUNT(*) FROM %s WHERE %s LIKE ?'):format(tabelle, spalte),
|
|
{ praefix .. '-' .. jahr .. '-%' }) or 0
|
|
return ('%s-%s-%04d'):format(praefix, jahr, anzahl + 1)
|
|
end
|