diff --git a/html/css/office.css b/html/css/office.css
index 539f947..9c84f0c 100644
--- a/html/css/office.css
+++ b/html/css/office.css
@@ -240,13 +240,23 @@ header.office-header h1 {
.monitor-event-line {
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
+ /* Grundfarbe fuer Schweregrade ohne eigene Farbe (z.B. info) - ohne sie stuende der Text
+ schwarz auf dem dunklen Monitor. */
+ fill: #cbd5e1;
}
-.severity-warning {
+/* Kopfzeile eines Events (Zeit + System): tritt gegenueber der Meldung zurueck. */
+.monitor-event-meta {
+ opacity: 0.75;
+}
+
+.severity-warning,
+.severity-warning .monitor-event-line {
fill: #facc15;
}
-.severity-error {
+.severity-error,
+.severity-error .monitor-event-line {
fill: #f87171;
}
diff --git a/html/js/office-render.js b/html/js/office-render.js
index 2b712b4..9d0aaab 100644
--- a/html/js/office-render.js
+++ b/html/js/office-render.js
@@ -20,6 +20,8 @@ import {
groupTicketsByColumn,
buildTicketModalContent,
mapEventForDisplay,
+ formatEventLineText,
+ truncateToWidth,
buildEventModalContent,
buildSystemModalContent,
buildKpiTiles,
@@ -85,6 +87,12 @@ const DESK_ROW_Y = [540, 840];
const DESK_COL_X = [232, 644, 1056, 1468];
const COFFEE_CORNER = { x: 960, y: 780 };
+// Ein Event belegt im Event-Monitor zwei Zeilen (Kopfzeile Zeit/System, darunter die
+// Meldung); EVENT_BLOCK_H ist der Abstand von Event zu Event, EVENT_MESSAGE_DY der der
+// beiden Zeilen zueinander. MAX_EVENT_LINES (5) Bloecke passen damit in MONITOR_WALL.h.
+const EVENT_BLOCK_H = 44;
+const EVENT_MESSAGE_DY = 18;
+
// Fester Platz des Product Owner (Issue #19): bewusst ausserhalb des dynamischen 8er-Rasters
// (DESK_COL_X/DESK_ROW_Y), in der freien Nische zwischen Board/Monitor-Wand und der ersten
// Schreibtischreihe - so kollidiert er nie mit einem echten, ueber die API registrierten Agenten.
@@ -286,14 +294,38 @@ function renderEventMonitor(frame, events) {
})
);
+ // Meldungen sind beliebig lang, der Monitor ist es nicht - einzeilig lief die Zeile ueber
+ // den Rahmen hinaus in die Szene. Jedes Event bekommt deshalb zwei Zeilen: Zeit + System
+ // oben, die Meldung eingerueckt darunter. Beide werden auf die Innenbreite gekuerzt, den
+ // Volltext zeigt der
-Tooltip beim Hover ueber das Event.
+ const textX = frame.x + 18;
+ const availableWidth = frame.w - 36;
+ const messageIndent = 12;
+
lines.forEach((eventLine, index) => {
- g.appendChild(
- text(`${eventLine.time} ${eventLine.system}: ${eventLine.message}`, {
- x: frame.x + 18,
- y: frame.y + 38 + index * 26,
- class: `monitor-event-line severity-${eventLine.severity}`,
+ const blockY = frame.y + 38 + index * EVENT_BLOCK_H;
+ const entry = el("g", { class: `monitor-event severity-${eventLine.severity}` });
+
+ const title = el("title");
+ title.textContent = formatEventLineText(eventLine);
+ entry.appendChild(title);
+
+ entry.appendChild(
+ text(truncateToWidth(`${eventLine.time} ${eventLine.system}`, availableWidth), {
+ x: textX,
+ y: blockY,
+ class: "monitor-event-line monitor-event-meta",
})
);
+ entry.appendChild(
+ text(truncateToWidth(eventLine.message, availableWidth - messageIndent), {
+ x: textX + messageIndent,
+ y: blockY + EVENT_MESSAGE_DY,
+ class: "monitor-event-line monitor-event-message",
+ })
+ );
+
+ g.appendChild(entry);
});
return g;
diff --git a/html/js/office-scene.js b/html/js/office-scene.js
index 1efe51f..7aa9dca 100644
--- a/html/js/office-scene.js
+++ b/html/js/office-scene.js
@@ -255,6 +255,48 @@ export function mapEventForDisplay(event) {
};
}
+/**
+ * Volltext einer Event-Zeile, wie er ungekuerzt im Tooltip steht.
+ *
+ * @param {{time: string, system: string, message: string}} eventLine
+ * @returns {string}
+ */
+export function formatEventLineText(eventLine) {
+ return `${eventLine.time} ${eventLine.system}: ${eventLine.message}`;
+}
+
+// Zeichenbreite der Monospace-Schrift des Event-Monitors (13px, siehe .monitor-event-line in
+// office.css). Bei Monospace ist die Breite fuer alle Zeichen gleich; der Wert ist am
+// breitesten der in Frage kommenden Schriften ausgerichtet (Courier New / DejaVu Sans Mono,
+// ~0.6 em; Consolas ist schmaler), damit die Zeile auf keinem System ueberlaeuft. So bleibt
+// die Abschaetzung ohne DOM-Messung auskommend und damit testbar.
+export const EVENT_LINE_CHAR_WIDTH = 7.9;
+
+/**
+ * Kuerzt einen Text auf die Breite, die im Monitor tatsaechlich zur Verfuegung steht, und
+ * markiert die Kuerzung mit einer Ellipse. Zu lange Zeilen liefen sonst ueber den
+ * Monitorrahmen hinaus in die Szene hinein; die volle Meldung zeigt der Tooltip
+ * (siehe renderEventMonitor()).
+ *
+ * @param {string} value
+ * @param {number} availableWidth - verfuegbare Breite in SVG-Einheiten
+ * @param {number} [charWidth]
+ * @returns {string}
+ */
+export function truncateToWidth(value, availableWidth, charWidth = EVENT_LINE_CHAR_WIDTH) {
+ const maxChars = Math.floor(availableWidth / charWidth);
+ if (maxChars <= 0) {
+ return "";
+ }
+ if (value.length <= maxChars) {
+ return value;
+ }
+ if (maxChars === 1) {
+ return "…";
+ }
+ return `${value.slice(0, maxChars - 1).trimEnd()}…`;
+}
+
/**
* Inhaltsmodell fuer das Event-Monitor-Modal: volle Liste (kein MAX_EVENT_LINES-Limit).
*
diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js
index 853a545..aee67d8 100644
--- a/tests/frontend/office-scene.test.js
+++ b/tests/frontend/office-scene.test.js
@@ -15,6 +15,9 @@ import {
buildTicketModalContent,
formatEventTime,
mapEventForDisplay,
+ formatEventLineText,
+ truncateToWidth,
+ EVENT_LINE_CHAR_WIDTH,
buildEventModalContent,
buildSystemModalContent,
buildKpiSparklinePoints,
@@ -286,6 +289,32 @@ test("mapEventForDisplay uebersetzt ein rohes Event in die Wand-Zeilendarstellun
assert.match(line.time, /^\d{2}:\d{2}$/);
});
+test("formatEventLineText setzt Zeit, System und Meldung zur Monitorzeile zusammen", () => {
+ assert.equal(
+ formatEventLineText({ time: "14:32", system: "Datenbank", message: "Verbindung weg" }),
+ "14:32 Datenbank: Verbindung weg"
+ );
+});
+
+test("truncateToWidth laesst passende Zeilen unveraendert", () => {
+ const value = "14:32 DB: weg";
+ assert.equal(truncateToWidth(value, value.length * EVENT_LINE_CHAR_WIDTH), value);
+});
+
+test("truncateToWidth kuerzt zu lange Zeilen auf die verfuegbare Breite inkl. Ellipse", () => {
+ const value = "14:32 Datenbank: Verbindung fehlgeschlagen";
+ const truncated = truncateToWidth(value, 10 * EVENT_LINE_CHAR_WIDTH);
+ assert.equal(truncated.length, 10);
+ assert.ok(truncated.endsWith("…"));
+ assert.ok(value.startsWith(truncated.slice(0, -1)));
+});
+
+test("truncateToWidth kommt mit entarteten Breiten klar", () => {
+ assert.equal(truncateToWidth("abc", 0), "");
+ assert.equal(truncateToWidth("abc", -5), "");
+ assert.equal(truncateToWidth("abc", EVENT_LINE_CHAR_WIDTH), "…");
+});
+
test("buildEventModalContent zeigt die volle Liste (kein MAX_EVENT_LINES-Limit)", () => {
const events = Array.from({ length: MAX_EVENT_LINES + 3 }, (_, i) => ({
occurred_at: "2026-07-27T10:00:00Z",