Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FILES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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】
Expand Down
72 changes: 72 additions & 0 deletions backend/src/db/combat-log.js
Original file line number Diff line number Diff line change
@@ -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);
}
29 changes: 3 additions & 26 deletions backend/src/db/mysql.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import mysql from 'mysql2/promise';
import { serializeCombatLogPayload } from './combat-log.js';

export default {
async connect(cfg) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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(
Expand Down
31 changes: 3 additions & 28 deletions backend/src/db/sqlite.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import { serializeCombatLogPayload } from './combat-log.js';

export default {
async connect({ file }) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions docs/module-usage-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down