diff --git a/FILES.md b/FILES.md index 8655a58..4b309dd 100644 --- a/FILES.md +++ b/FILES.md @@ -18,6 +18,7 @@ This document summarizes the purpose of the key files and directories that make - `src/auth.js` – JWT-based authentication helpers used by HTTP routes and WebSocket handshakes, including middleware for enforcing admin access.【F:backend/src/auth.js†L1-L36】 - `src/permissions.js` – Normalises role definitions, checks per-server capabilities, filters data by access level, and serialises permission payloads.【F:backend/src/permissions.js†L1-L120】 - `src/db/index.js` – Chooses the configured database driver (SQLite or MySQL), ensures schema migrations, seeds default roles, and provisions the first admin account.【F:backend/src/db/index.js†L1-L63】 +- `src/db/combat-log.js` – Shared helper that serialises combat log payloads for database storage while trimming them to fit the 8 KB column limit without corrupting the JSON structure.【F:backend/src/db/combat-log.js†L1-L72】 - `src/db/sqlite.js` – SQLite-backed implementation of the database API, providing CRUD helpers for users, servers, roles, telemetry records, and chat logs (including scope and colour metadata).【F:backend/src/db/sqlite.js†L1-L220】 - `src/db/mysql.js` – MySQL-backed implementation of the database API, including table creation statements and query helpers for user, server, player, and chat history data.【F:backend/src/db/mysql.js†L1-L200】 - `src/rcon.js` – Robust WebRCON client that maintains persistent connections, queues commands, handles keepalive traffic, and emits structured events for the rest of the app.【F:backend/src/rcon.js†L1-L120】 diff --git a/backend/src/db/combat-log.js b/backend/src/db/combat-log.js new file mode 100644 index 0000000..e569709 --- /dev/null +++ b/backend/src/db/combat-log.js @@ -0,0 +1,72 @@ +const COMBAT_LOG_MAX_LENGTH = 8000; + +function ensureMaxLength(text) { + if (typeof text !== 'string') return null; + return text.length > COMBAT_LOG_MAX_LENGTH ? text.slice(0, COMBAT_LOG_MAX_LENGTH) : text; +} + +export function serializeCombatLogPayload(combatPayload) { + if (combatPayload == null) return null; + + if (typeof combatPayload === 'string') { + return ensureMaxLength(combatPayload); + } + + if (typeof combatPayload !== 'object') { + return null; + } + + const payload = { ...combatPayload }; + if (Array.isArray(payload.lines)) payload.lines = payload.lines.slice(); + if (Array.isArray(payload.records)) payload.records = payload.records.slice(); + + const encode = () => { + try { + return JSON.stringify(payload); + } catch { + return null; + } + }; + + let json = encode(); + if (json == null) return null; + if (json.length <= COMBAT_LOG_MAX_LENGTH) return json; + + const shrinkArray = (key) => { + const arr = payload[key]; + if (!Array.isArray(arr) || arr.length === 0) return; + while (arr.length > 0) { + arr.pop(); + const encoded = encode(); + if (encoded == null) { + json = null; + return; + } + json = encoded; + if (json.length <= COMBAT_LOG_MAX_LENGTH) return; + } + }; + + shrinkArray('records'); + if (json != null && json.length <= COMBAT_LOG_MAX_LENGTH) return json; + + shrinkArray('lines'); + if (json != null && json.length <= COMBAT_LOG_MAX_LENGTH) return json; + + if (typeof payload.text === 'string' && payload.text.length > 0) { + const overBy = json ? json.length - COMBAT_LOG_MAX_LENGTH : payload.text.length; + const targetLength = Math.max(0, payload.text.length - overBy); + payload.text = payload.text.slice(0, targetLength); + json = encode(); + if (json != null && json.length <= COMBAT_LOG_MAX_LENGTH) return json; + } + + const fallbackText = typeof combatPayload.text === 'string' + ? combatPayload.text + : Array.isArray(combatPayload.lines) + ? combatPayload.lines.join('\n') + : ''; + + if (!fallbackText) return null; + return ensureMaxLength(fallbackText); +} diff --git a/backend/src/db/mysql.js b/backend/src/db/mysql.js index 4247214..3d8afb2 100644 --- a/backend/src/db/mysql.js +++ b/backend/src/db/mysql.js @@ -1,4 +1,5 @@ import mysql from 'mysql2/promise'; +import { serializeCombatLogPayload } from './combat-log.js'; export default { async connect(cfg) { @@ -1014,20 +1015,8 @@ function createApi(pool, dialect) { const posY = Number.isFinite(posYRaw) ? posYRaw : null; const posZ = Number.isFinite(posZRaw) ? posZRaw : null; const rawLine = trimOrNull(entry?.raw); - let combatLogSerialized = null; const combatPayload = entry?.combat_log ?? entry?.combatLog ?? entry?.combat_log_json ?? entry?.combatLogJson; - if (combatPayload != null) { - if (typeof combatPayload === 'string') { - combatLogSerialized = combatPayload.length > 8000 ? combatPayload.slice(0, 8000) : combatPayload; - } else { - try { - const json = JSON.stringify(combatPayload); - combatLogSerialized = json.length > 8000 ? json.slice(0, 8000) : json; - } catch { - combatLogSerialized = null; - } - } - } + const combatLogSerialized = serializeCombatLogPayload(combatPayload); const combatErrorRaw = trimOrNull(entry?.combat_log_error ?? entry?.combatLogError); const combatLogError = combatErrorRaw ? combatErrorRaw.slice(0, 500) : null; const createdAt = normaliseDateTime(entry?.created_at ?? entry?.createdAt) || normaliseDateTime(new Date()); @@ -1084,20 +1073,8 @@ function createApi(pool, dialect) { const serverIdNum = Number(entry?.server_id ?? entry?.serverId); const eventId = Number(entry?.id ?? entry?.eventId); if (!Number.isFinite(serverIdNum) || !Number.isFinite(eventId)) return 0; - let combatLogSerialized = null; const combatPayload = entry?.combat_log ?? entry?.combatLog ?? entry?.combat_log_json ?? entry?.combatLogJson; - if (combatPayload != null) { - if (typeof combatPayload === 'string') { - combatLogSerialized = combatPayload.length > 8000 ? combatPayload.slice(0, 8000) : combatPayload; - } else { - try { - const json = JSON.stringify(combatPayload); - combatLogSerialized = json.length > 8000 ? json.slice(0, 8000) : json; - } catch { - combatLogSerialized = null; - } - } - } + const combatLogSerialized = serializeCombatLogPayload(combatPayload); const combatErrorRaw = trimOrNull(entry?.combat_log_error ?? entry?.combatLogError); const combatLogError = combatErrorRaw ? combatErrorRaw.slice(0, 500) : null; const result = await exec( diff --git a/backend/src/db/sqlite.js b/backend/src/db/sqlite.js index 0957cda..46da492 100644 --- a/backend/src/db/sqlite.js +++ b/backend/src/db/sqlite.js @@ -1,5 +1,6 @@ import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; +import { serializeCombatLogPayload } from './combat-log.js'; export default { async connect({ file }) { @@ -1069,21 +1070,8 @@ function createApi(dbh, dialect) { const posY = Number.isFinite(posYRaw) ? posYRaw : null; const posZ = Number.isFinite(posZRaw) ? posZRaw : null; const rawLine = trimOrNull(entry?.raw); - let combatLogSerialized = null; const combatPayload = entry?.combat_log ?? entry?.combatLog ?? entry?.combat_log_json ?? entry?.combatLogJson; - if (combatPayload != null) { - if (typeof combatPayload === 'string') { - const text = combatPayload.length > 8000 ? combatPayload.slice(0, 8000) : combatPayload; - combatLogSerialized = text; - } else { - try { - const json = JSON.stringify(combatPayload); - combatLogSerialized = json.length > 8000 ? json.slice(0, 8000) : json; - } catch { - combatLogSerialized = null; - } - } - } + const combatLogSerialized = serializeCombatLogPayload(combatPayload); const combatErrorRaw = trimOrNull(entry?.combat_log_error ?? entry?.combatLogError); const combatLogError = combatErrorRaw ? combatErrorRaw.slice(0, 500) : null; const createdAt = normaliseIso(entry?.created_at ?? entry?.createdAt) || new Date().toISOString(); @@ -1140,21 +1128,8 @@ function createApi(dbh, dialect) { const serverIdNum = Number(entry?.server_id ?? entry?.serverId); const eventId = Number(entry?.id ?? entry?.eventId); if (!Number.isFinite(serverIdNum) || !Number.isFinite(eventId)) return 0; - let combatLogSerialized = null; const combatPayload = entry?.combat_log ?? entry?.combatLog ?? entry?.combat_log_json ?? entry?.combatLogJson; - if (combatPayload != null) { - if (typeof combatPayload === 'string') { - const text = combatPayload.length > 8000 ? combatPayload.slice(0, 8000) : combatPayload; - combatLogSerialized = text; - } else { - try { - const json = JSON.stringify(combatPayload); - combatLogSerialized = json.length > 8000 ? json.slice(0, 8000) : json; - } catch { - combatLogSerialized = null; - } - } - } + const combatLogSerialized = serializeCombatLogPayload(combatPayload); const combatErrorRaw = trimOrNull(entry?.combat_log_error ?? entry?.combatLogError); const combatLogError = combatErrorRaw ? combatErrorRaw.slice(0, 500) : null; const result = await dbh.run( diff --git a/docs/module-usage-report.md b/docs/module-usage-report.md index f7bb46a..850201e 100644 --- a/docs/module-usage-report.md +++ b/docs/module-usage-report.md @@ -9,6 +9,7 @@ This document summarises how the major backend and frontend modules are referenc | `backend/src/index.js` | Main Express application wiring HTTP, WebSocket, and module integrations. | Entry point configured as the package main script. | Serves as the central orchestrator, including kill-feed parsing/combat-log enrichment before broadcasting events and the REST API for player notes.【F:backend/package.json†L1-L21】【F:backend/src/index.js†L708-L846】【F:backend/src/index.js†L2212-L2240】【F:backend/src/index.js†L6374-L6429】 | | `backend/src/auth.js` | JWT helpers (`signToken`) and auth middleware/guard used by API routes. | Imported by the API entry point to secure routes and admin-only endpoints.【F:backend/src/auth.js†L1-L41】【F:backend/src/index.js†L13-L50】 | Active middleware — not redundant. | | `backend/src/db/index.js` | Chooses SQLite or MySQL client, initialises schema, seeds default roles/users, and exposes the `db` API. | Required by both the HTTP server and Discord worker to persist data.【F:backend/src/db/index.js†L1-L110】【F:backend/src/index.js†L13-L40】【F:backend/src/discord-bot-service.js†L16-L28】 | Central DB abstraction; keeps dialect-specific files necessary. | +| `backend/src/db/combat-log.js` | Serialises combat log payloads for storage while pruning oversized arrays/strings. | Imported by both DB dialect implementations to cap combat log blobs before persistence.【F:backend/src/db/combat-log.js†L1-L72】【F:backend/src/db/sqlite.js†L1048-L1138】【F:backend/src/db/mysql.js†L1000-L1085】 | Ensures stored combat logs stay parseable instead of truncating JSON mid-structure. | | `backend/src/db/sqlite.js` | Implements the SQLite dialect by opening a file database and creating tables/columns used throughout the app. | Loaded through `db/index.js` when `DB_CLIENT` defaults to SQLite.【F:backend/src/db/sqlite.js†L1-L200】【F:backend/src/db/index.js†L3-L24】 | Required for the default deployment path. | | `backend/src/db/mysql.js` | Provides the MySQL dialect with equivalent schema management and query helpers. | Loaded through `db/index.js` when `DB_CLIENT=mysql` is set.【F:backend/src/db/mysql.js†L1-L200】【F:backend/src/db/index.js†L3-L17】 | Optional runtime dependency but still in active use for MySQL deployments. | | `backend/src/permissions.js` | Normalises/serialises role capabilities and performs permission checks per server/global scope. | Consumed by the HTTP API to filter resources and by the DB bootstrap to seed default roles.【F:backend/src/permissions.js†L1-L158】【F:backend/src/index.js†L42-L50】【F:backend/src/db/index.js†L38-L110】 | Required for RBAC; not redundant. |