diff --git a/server/src/services/replayEngineV2.js b/server/src/services/replayEngineV2.js
index a0fb3a45..9fc441cd 100644
--- a/server/src/services/replayEngineV2.js
+++ b/server/src/services/replayEngineV2.js
@@ -405,7 +405,12 @@ function buildChatEventsForWindow(startMs, endMs, limit = 18) {
const text = String(msg?.text || '').replace(/\s+/g, ' ').trim();
return {
ts: Number(msg.ts),
- text: `${nickname}: ${text}`.slice(0, 120),
+ nickname: nickname.slice(0, 32),
+ text: text.slice(0, 120),
+ role: String(msg?.role || ''),
+ fromDiscord: Boolean(msg?.fromDiscord),
+ roverId: msg?.roverId ? String(msg.roverId) : '',
+ roverColor: msg?.roverColor ? String(msg.roverColor) : '',
};
});
}
@@ -436,6 +441,31 @@ function wrapTextLines(text, maxChars = 28) {
return lines.slice(0, 4);
}
+function hexToRgb(hex) {
+ const value = String(hex || '').trim();
+ const match = /^#([0-9A-Fa-f]{6})$/.exec(value);
+ if (!match) return null;
+ const raw = match[1];
+ return {
+ r: parseInt(raw.slice(0, 2), 16),
+ g: parseInt(raw.slice(2, 4), 16),
+ b: parseInt(raw.slice(4, 6), 16),
+ };
+}
+
+function roleColor(role = '') {
+ switch (String(role)) {
+ case 'admin':
+ case 'lockdown':
+ case 'lockdown-admin':
+ return '#FCD34D'; // amber-300
+ case 'spectator':
+ return '#94A3B8'; // slate-400
+ default:
+ return '#7DD3FC'; // sky-300
+ }
+}
+
function renderSidebarSvg({
width,
height,
@@ -445,63 +475,87 @@ function renderSidebarSvg({
}) {
const titleParts = wrapTextLines(title, 22).slice(0, 4).map((line) => escapeXml(line));
const statLines = driverBatteryLines.slice(0, 8).map((line) => escapeXml(line));
- const normalizedChats = chatLines
- .slice(-12)
- .map((line) => wrapTextLines(line, 24).slice(0, 3).map((wrapped) => escapeXml(wrapped)));
+ const normalizedChats = chatLines.slice(-12).map((entry) => {
+ const wrapped = wrapTextLines(entry.text || '', 24).slice(0, 3).map((line) => escapeXml(line));
+ const nick = escapeXml(entry.nickname || 'user');
+ const roverId = escapeXml(entry.roverId || '');
+ const roverRgb = hexToRgb(entry.roverColor || '');
+ const roverBadgeBg = roverRgb ? `rgba(${roverRgb.r},${roverRgb.g},${roverRgb.b},0.18)` : 'rgba(30,41,59,0.70)';
+ const roverBadgeBorder = roverRgb ? `rgba(${roverRgb.r},${roverRgb.g},${roverRgb.b},0.60)` : 'rgba(71,85,105,0.75)';
+ return {
+ nick,
+ wrapped,
+ nameColor: roleColor(entry.role),
+ fromDiscord: Boolean(entry.fromDiscord),
+ roverId,
+ roverBadgeBg,
+ roverBadgeBorder,
+ bubbleTone: entry.fromDiscord ? 'discordBubble' : 'chatBubble',
+ };
+ });
const shapes = [];
const textRows = [];
- const pad = 12;
+ const pad = 8;
const cardX = pad;
const cardW = width - pad * 2;
- let y = 12;
+ let y = 8;
// Title card
- const titleCardH = Math.max(64, 22 + titleParts.length * 24);
- shapes.push(``);
+ const titleCardH = Math.max(56, 18 + titleParts.length * 22);
+ shapes.push(``);
let ty = y + 28;
for (const part of titleParts) {
textRows.push(`${part}`);
ty += 22;
}
- y += titleCardH + 10;
+ y += titleCardH + 6;
// Drivers card
const driverLines = statLines.length ? statLines : ['No active drivers'];
- const driversCardH = 28 + driverLines.length * 20 + 10;
- shapes.push(``);
+ const driversCardH = 24 + driverLines.length * 18 + 8;
+ shapes.push(``);
textRows.push(`Drivers`);
let dy = y + 44;
for (const line of driverLines) {
textRows.push(`${line}`);
dy += 20;
}
- y += driversCardH + 10;
+ y += driversCardH + 6;
// Chat card
- const chatCardH = Math.max(80, height - y - 12);
- shapes.push(``);
+ const chatCardH = Math.max(80, height - y - 8);
+ shapes.push(``);
textRows.push(`Chat`);
let cy = y + 34;
- const bubbleX = cardX + 10;
- const bubbleW = cardW - 20;
+ const bubbleX = cardX + 8;
+ const bubbleW = cardW - 16;
if (!normalizedChats.length) {
textRows.push(`No chat in replay window`);
} else {
for (let i = 0; i < normalizedChats.length; i += 1) {
const block = normalizedChats[i];
- const bubbleH = 10 + block.length * 18;
+ const bubbleH = 26 + block.wrapped.length * 16;
if (cy + bubbleH + 6 > y + chatCardH - 8) break;
shapes.push(
- ``,
+ ``,
);
- let by = cy + 18;
- for (const line of block) {
- textRows.push(`${line}`);
- by += 18;
+ textRows.push(`${block.nick}`);
+ if (block.fromDiscord) {
+ textRows.push(`◈`);
+ }
+ if (block.roverId) {
+ const badgeTextX = bubbleX + bubbleW - 70;
+ shapes.push(
+ ``,
+ );
+ textRows.push(`${block.roverId}`);
+ }
+ let by = cy + 32;
+ for (const line of block.wrapped) {
+ textRows.push(`${line}`);
+ by += 16;
}
cy += bubbleH + 6;
}
@@ -515,17 +569,20 @@ function renderSidebarSvg({
-
-
+
+
${shapes.join('\n ')}
${textRows.join('\n ')}