diff --git a/html/css/office.css b/html/css/office.css
index 5709c78..02ef5e2 100644
--- a/html/css/office.css
+++ b/html/css/office.css
@@ -212,6 +212,17 @@ header.office-header h1 {
stroke: rgba(15, 23, 42, 0.35);
}
+/* Issue #91: Ticket-Nummer auf dem Zettel; heller Rand haelt sie auf dunklen Projektfarben lesbar. */
+.note-number {
+ fill: #0f172a;
+ font-size: 16px;
+ font-weight: 700;
+ paint-order: stroke;
+ stroke: rgba(255, 255, 255, 0.8);
+ stroke-width: 3px;
+ pointer-events: none;
+}
+
.note-stack-badge {
fill: var(--color-note-stack-badge);
stroke: #92400e;
diff --git a/html/js/office-render.js b/html/js/office-render.js
index fec947e..b4a77ae 100644
--- a/html/js/office-render.js
+++ b/html/js/office-render.js
@@ -299,6 +299,16 @@ function renderBoard(ticketCounts, groupedTickets, onColumnClick, onReloadClick)
column.noteColors[i]
)
);
+ if (column.noteNumbers?.[i]) {
+ colGroup.appendChild(
+ text(column.noteNumbers[i], {
+ x: noteX + NOTE_W / 2,
+ y: noteY + NOTE_H / 2 + 6,
+ class: "note-number",
+ "text-anchor": "middle",
+ })
+ );
+ }
noteY += NOTE_STEP_Y;
}
diff --git a/html/js/office-scene.js b/html/js/office-scene.js
index c3e0b88..28b835e 100644
--- a/html/js/office-scene.js
+++ b/html/js/office-scene.js
@@ -50,7 +50,7 @@ export const NOTE_STACK_LAYERS = 3;
* @param {Record>} groupedTickets - Tickets je Spalte in derselben
* Reihenfolge wie gezaehlt (siehe groupTicketsByColumn()).
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number,
- * stackCount: number, noteColors: Array, stackColors: Array}>}
+ * stackCount: number, noteColors: Array, noteNumbers: Array, stackColors: Array}>}
*/
export function buildBoardColumnLayout(ticketCounts = {}, groupedTickets = {}) {
return BOARD_COLUMNS.map(({ key, label }) => {
@@ -64,6 +64,7 @@ export function buildBoardColumnLayout(ticketCounts = {}, groupedTickets = {}) {
visibleNotes,
stackCount,
noteColors: noteColors(tickets.slice(0, visibleNotes), visibleNotes),
+ noteNumbers: noteNumbers(tickets.slice(0, visibleNotes), visibleNotes),
stackColors: stackCount > 0 ? stackLayerColors(tickets.slice(visibleNotes)) : [],
});
@@ -85,6 +86,15 @@ function noteColors(tickets, length) {
return Array.from({ length }, (_, i) => tickets[i]?.color ?? null);
}
+// Issue #91: Ticket-Nummer auf dem Zettel (nur Codeberg-Tickets haben eine); null bei
+// Tickets ohne Nummer und im Demo-Modus. Der Stapel zeigt bewusst keine Nummern.
+function noteNumbers(tickets, length) {
+ return Array.from({ length }, (_, i) => {
+ const ticket = tickets[i];
+ return ticket?.source === "codeberg" && ticket.external_ref ? `#${ticket.external_ref}` : null;
+ });
+}
+
// Ein Stapel steht fuer viele Tickets, aber nur fuer NOTE_STACK_LAYERS Zettel: er zeigt
// deshalb die ersten verschiedenen Projektfarben darin, statt eine davon dreimal zu
// wiederholen. So ist auch an "Done" (immer ein Stapel) zu sehen, aus welchen Projekten die
diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js
index d81fa2e..663f219 100644
--- a/tests/frontend/office-scene.test.js
+++ b/tests/frontend/office-scene.test.js
@@ -112,6 +112,22 @@ test("buildBoardColumnLayout faerbt jeden Zettel in der Farbe seines Projekts (I
assert.deepEqual(bereit.noteColors, ["#fbca04", "#28a745", "#fbca04"]);
});
+test("buildBoardColumnLayout liefert Ticket-Nummern fuer Zettel, nicht fuer den Stapel (Issue #91)", () => {
+ const tickets = Array.from({ length: 7 }, (_, i) => ({
+ column_name: "bereit",
+ source: "codeberg",
+ external_ref: String(i + 1),
+ }));
+ tickets[1] = { column_name: "bereit", source: "api" };
+ const bereit = buildBoardColumnLayout(
+ aggregateTicketsByColumn(tickets),
+ groupTicketsByColumn(tickets)
+ ).find((c) => c.key === "bereit");
+
+ assert.deepEqual(bereit.noteNumbers, ["#1", null, "#3", "#4"]);
+ assert.equal(bereit.stackCount, 3);
+});
+
test("buildBoardColumnLayout laesst Zettel ohne Projektfarbe ungefaerbt", () => {
const tickets = [{ column_name: "qa" }, { column_name: "qa", color: "#0366d6" }];
const qa = buildBoardColumnLayout(