diff --git a/README.md b/README.md
index ee7ff60..95a260e 100644
--- a/README.md
+++ b/README.md
@@ -183,13 +183,23 @@ Threads werden nicht immer zuverlässig verschoben, ein längst genehmigter Antr
Antrags-Board liegen. Das aktuelle Board steht deshalb nur als Hinweis an der Zeile.
Pro Antrag wählt ein Admin: *angenommen als neuer CC*, *angenommen als Redesign* (mit Auswahl,
-welcher CC ersetzt wurde und woraus bezahlt wird), *abgelehnt* oder *kein Antrag*. Die beiden
+welcher CC ersetzt wurde und woraus bezahlt wird), *angenommen als Reaktivierung* (ein inaktiver
+CC kehrt zurück — gratis, sofern er nicht redesignt wurde), *abgelehnt* oder *kein Antrag*. Die beiden
angenommenen Fälle lösen exakt dieselben Buchungen aus wie die Knöpfe auf der Spielerseite —
es gibt keinen zweiten Buchungsweg mit eigenen Regeln.
Zugeordnet wird über den Forumsnamen des Erstellers gegen das Feld *Forum-Name* beim Spieler.
-Ohne Treffer bleibt der Antrag in der Liste und lässt sich von Hand zuordnen; trägst du den
-Forumsnamen beim Spieler nach, findet ihn der nächste Abgleich von allein.
+**Threads ohne zuordenbaren Spieler werden gar nicht erst erfasst** — das Archiv reicht Jahre
+zurück und enthält vieles, was kein CC-Antrag ist. Fehlt jemand, trag seinen Forumsnamen beim
+Spieler nach, dann nimmt ihn der nächste Abgleich auf.
+
+Angepinnte Ankündigungen (Changelogs, Regelwerke) liegen mit in den Boards. Sie werden über ihre
+Labels *Wichtig* und *Information* aussortiert. Der Titel wird dagegen **nie** ausgewertet: CC-Anträge
+heißen mal „CC Antrag | Name", mal „\"The Duke\" Rang Update" — daran ist nichts zu erkennen.
+
+Trägst du einen Antrag von Hand ein — über *Neuer CC* oder *Redesign* auf der Spielerseite —
+kannst du dort den Thread-Link angeben. Der Antrag gilt damit als erledigt und taucht beim
+nächsten Abgleich nicht erneut auf.
Ein Thread behält seine ID, auch wenn er zwischen den Boards verschoben wird — daran hängt die
Wiedererkennung. Wandert ein **bereits entschiedener** Antrag später in ein anderes Board, etwa
diff --git a/server/prisma/migrations/20250103000000_reaktivierung_und_labels/migration.sql b/server/prisma/migrations/20250103000000_reaktivierung_und_labels/migration.sql
new file mode 100644
index 0000000..b5c8bdb
--- /dev/null
+++ b/server/prisma/migrations/20250103000000_reaktivierung_und_labels/migration.sql
@@ -0,0 +1,8 @@
+-- Reaktivierung als eigene Entscheidung, Labels und Thread-Verknuepfung.
+
+DO $$ BEGIN
+ ALTER TYPE "ForumClassification" ADD VALUE IF NOT EXISTS 'REACTIVATION';
+EXCEPTION WHEN undefined_object THEN NULL; END $$;
+
+-- Labels des Threads, nur zur Anzeige als Kontext.
+ALTER TABLE "ForumThread" ADD COLUMN IF NOT EXISTS "labels" TEXT;
diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma
index d787197..e42a156 100644
--- a/server/prisma/schema.prisma
+++ b/server/prisma/schema.prisma
@@ -55,6 +55,8 @@ enum ForumBoard {
enum ForumClassification {
NEW_CC
REDESIGN
+ /// Ein inaktiver CC kehrt zurueck. Ohne Redesign kostet das nichts.
+ REACTIVATION
REJECTED
IGNORE
}
@@ -317,6 +319,8 @@ model ForumThread {
authorName String?
/// Überlebt Namensänderungen im Forum, anders als der Anzeigename.
authorForumId Int?
+ /// Labels des Threads, kommagetrennt. Reiner Kontext für die Entscheidung.
+ labels String?
playerId Int?
player Player? @relation(fields: [playerId], references: [id], onDelete: SetNull)
diff --git a/server/src/lib/forum.ts b/server/src/lib/forum.ts
index bb2eafe..e16bd9b 100644
--- a/server/src/lib/forum.ts
+++ b/server/src/lib/forum.ts
@@ -20,6 +20,16 @@ export interface ForumThread {
authorForumId: number | null;
/// WoltLab lässt beim Verschieben optional einen Verweis am alten Ort zurück.
isMovedStub: boolean;
+ /// Labels des Threads, etwa "Wichtig" oder "SCP-RP".
+ labels: string[];
+}
+
+/// Angepinnte Ankündigungen liegen mit in den CC-Boards - Changelogs,
+/// Regelwerke, Bewerbungsphasen. Sie tragen diese Labels und sind keine Anträge.
+export const IGNORED_LABELS = ['wichtig', 'information'];
+
+export function hasIgnoredLabel(thread: ForumThread): boolean {
+ return thread.labels.some((label) => IGNORED_LABELS.includes(label.toLowerCase()));
}
const ROW_SPLIT = /
/;
@@ -32,6 +42,9 @@ const USER_LINK =
// Ein zurückgelassener Verweis ist kein eigener Thread.
const MOVED_MARKER = /wbbTopicLinkMoved|class="[^"]*movedTopic|>\s*Verschoben\s*]*class="[^"]*\bbadge label\b[^"]*"[^>]*>([^<]{1,40})<\/a>/g;
+
function decode(value: string): string {
return value
.replace(/"/g, '"')
@@ -59,6 +72,9 @@ export function parseBoard(html: string): ForumThread[] {
const user = USER_LINK.exec(block);
+ LABEL.lastIndex = 0;
+ const labels = [...block.matchAll(LABEL)].map((m) => decode(m[1] ?? '')).filter(Boolean);
+
threads.push({
threadId,
title: decode(topic[3] ?? ''),
@@ -66,6 +82,7 @@ export function parseBoard(html: string): ForumThread[] {
authorName: user ? decode(user[2] ?? '') || null : null,
authorForumId: user ? Number(user[1]) : null,
isMovedStub: MOVED_MARKER.test(block),
+ labels,
});
}
@@ -77,3 +94,18 @@ export function parsePageCount(html: string): number {
const numbers = [...html.matchAll(/pageNo=(\d+)/g)].map((m) => Number(m[1]));
return numbers.length > 0 ? Math.max(1, ...numbers) : 1;
}
+
+/**
+ * Zieht die Thread-ID aus einer Forums-URL. Angenommen werden der volle Link,
+ * der Pfad oder die blanke Nummer - beim Eintippen von Hand landet mal das
+ * eine, mal das andere im Feld.
+ */
+export function parseThreadId(input: string): number | null {
+ const value = input.trim();
+ if (value.length === 0) return null;
+
+ const fromUrl = /thread\/(\d+)/.exec(value);
+ if (fromUrl) return Number(fromUrl[1]);
+
+ return /^\d+$/.test(value) ? Number(value) : null;
+}
diff --git a/server/src/routes/characters.routes.ts b/server/src/routes/characters.routes.ts
index 4894bda..03f8a2b 100644
--- a/server/src/routes/characters.routes.ts
+++ b/server/src/routes/characters.routes.ts
@@ -11,8 +11,27 @@ import {
revertCharacter,
updateCharacter,
} from '../services/characters.js';
+import { parseThreadId } from '../lib/forum.js';
+import { linkThreadToCharacter } from '../services/forum.js';
+import { badRequest } from '../errors.js';
import { contextOf, idParam, optionalNote, parseWith } from './helpers.js';
+/** Hakt den zugehörigen Forums-Thread ab, wenn einer angegeben wurde. */
+async function linkThread(
+ threadInput: string | undefined,
+ characterId: number,
+ classification: 'NEW_CC' | 'REDESIGN',
+ ctx: ReturnType,
+): Promise {
+ if (!threadInput?.trim()) return;
+
+ const threadId = parseThreadId(threadInput);
+ if (threadId === null) {
+ throw badRequest('INVALID_THREAD', 'Der Thread-Link konnte nicht gelesen werden');
+ }
+ await linkThreadToCharacter(threadId, characterId, classification, ctx);
+}
+
const redesignKind = z.enum([CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]);
const reviveKind = z.enum([CreditKind.CC, CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]);
@@ -24,10 +43,13 @@ export async function characterRoutes(app: FastifyInstance): Promise {
name: z.string().trim().min(1).max(160),
loreText: z.string().trim().max(5000).optional(),
note: optionalNote,
+ forumThread: z.string().trim().max(300).optional(),
}),
request.body,
);
- const result = await createCharacter(id, body, contextOf(request));
+ const ctx = contextOf(request);
+ const result = await createCharacter(id, body, ctx);
+ await linkThread(body.forumThread, result.character.id, 'NEW_CC', ctx);
reply.code(201);
return result;
});
@@ -52,10 +74,14 @@ export async function characterRoutes(app: FastifyInstance): Promise {
loreText: z.string().trim().max(5000).optional(),
useKind: redesignKind,
note: optionalNote,
+ forumThread: z.string().trim().max(300).optional(),
}),
request.body,
);
- return redesignCharacter(id, body, contextOf(request));
+ const ctx = contextOf(request);
+ const result = await redesignCharacter(id, body, ctx);
+ await linkThread(body.forumThread, result.character.id, 'REDESIGN', ctx);
+ return result;
});
app.post('/api/characters/:id/deactivate', { preHandler: requireEditor }, async (request) => {
diff --git a/server/src/routes/forum.routes.ts b/server/src/routes/forum.routes.ts
index 2e6ee09..4213f73 100644
--- a/server/src/routes/forum.routes.ts
+++ b/server/src/routes/forum.routes.ts
@@ -64,7 +64,8 @@ export async function forumRoutes(app: FastifyInstance): Promise {
characterName: z.string().trim().min(1).max(160).optional(),
loreText: z.string().trim().max(5000).optional(),
predecessorId: z.number().int().positive().optional(),
- useKind: z.enum(['REDESIGN_PAID', 'REDESIGN_KULANZ']).optional(),
+ characterId: z.number().int().positive().optional(),
+ useKind: z.enum(['REDESIGN_PAID', 'REDESIGN_KULANZ', 'CC']).optional(),
}),
request.body,
);
diff --git a/server/src/services/forum.ts b/server/src/services/forum.ts
index beb2af7..44561a6 100644
--- a/server/src/services/forum.ts
+++ b/server/src/services/forum.ts
@@ -2,7 +2,7 @@ import { ForumBoard, ForumClassification, type Prisma } from '@prisma/client';
import { prisma } from '../db.js';
import { env } from '../env.js';
import { badRequest, conflict, notFound } from '../errors.js';
-import { parseBoard, parsePageCount, type ForumThread } from '../lib/forum.js';
+import { hasIgnoredLabel, parseBoard, parsePageCount, type ForumThread } from '../lib/forum.js';
import { recordAudit, type ActionContext } from './audit.js';
/// Die drei Boards, zwischen denen ein Antrag wandert. Board 66 enthält neben
@@ -61,8 +61,10 @@ export interface ForumScanSummary {
movedThreads: number;
boardChangeNotices: number;
skippedStubs: number;
+ skippedLabels: number;
+ /// Autor gehört zu keinem erfassten Spieler - das Archiv ist voll davon.
+ skippedUnknownAuthor: number;
matchedPlayers: number;
- withoutPlayer: number;
failedBoards: { board: string; reason: string }[];
}
@@ -79,8 +81,9 @@ export async function scanForum(ctx: ActionContext): Promise {
movedThreads: 0,
boardChangeNotices: 0,
skippedStubs: 0,
+ skippedLabels: 0,
+ skippedUnknownAuthor: 0,
matchedPlayers: 0,
- withoutPlayer: 0,
failedBoards: [],
};
@@ -121,6 +124,12 @@ export async function scanForum(ctx: ActionContext): Promise {
continue;
}
+ // Angepinnte Ankündigungen liegen mit in den Boards.
+ if (hasIgnoredLabel(thread)) {
+ summary.skippedLabels += 1;
+ continue;
+ }
+
const playerId = thread.authorName
? byForumName.get(thread.authorName.toLowerCase()) ?? null
: null;
@@ -129,6 +138,15 @@ export async function scanForum(ctx: ActionContext): Promise {
where: { threadId: thread.threadId },
});
+ // Das Archiv umfasst Jahre und enthält vieles, was kein CC-Antrag ist.
+ // Ohne zuordenbaren Spieler wird ein Thread deshalb gar nicht erst
+ // erfasst - sonst ertrinkt die Liste in Fremdem. Bereits bekannte
+ // Threads werden weiter gepflegt, auch wenn die Zuordnung fehlt.
+ if (!existing && playerId === null) {
+ summary.skippedUnknownAuthor += 1;
+ continue;
+ }
+
if (!existing) {
await prisma.forumThread.create({
data: {
@@ -137,14 +155,14 @@ export async function scanForum(ctx: ActionContext): Promise {
url: thread.url,
authorName: thread.authorName,
authorForumId: thread.authorForumId,
+ labels: thread.labels.join(', ') || null,
playerId,
currentBoard: board,
history: { create: [{ board }] },
},
});
summary.newThreads += 1;
- if (playerId) summary.matchedPlayers += 1;
- else summary.withoutPlayer += 1;
+ summary.matchedPlayers += 1;
continue;
}
@@ -157,6 +175,7 @@ export async function scanForum(ctx: ActionContext): Promise {
url: thread.url,
authorName: thread.authorName,
authorForumId: thread.authorForumId,
+ labels: thread.labels.join(', ') || null,
lastSeenAt: new Date(),
// Eine später nachgetragene Zuordnung nicht wieder überschreiben.
...(existing.playerId === null && playerId !== null ? { playerId } : {}),
@@ -268,7 +287,9 @@ export interface ClassifyInput {
loreText?: string;
/// Nur bei REDESIGN: welcher CC wurde ersetzt und womit bezahlt.
predecessorId?: number;
- useKind?: 'REDESIGN_PAID' | 'REDESIGN_KULANZ';
+ /// Nur bei REACTIVATION: welcher inaktive CC kehrt zurück.
+ characterId?: number;
+ useKind?: 'REDESIGN_PAID' | 'REDESIGN_KULANZ' | 'CC';
}
/**
@@ -289,7 +310,33 @@ export async function classifyThread(
let characterId: number | null = null;
- if (
+ if (input.classification === ForumClassification.REACTIVATION) {
+ if (input.characterId === undefined) {
+ throw badRequest('NO_CHARACTER', 'Bitte angeben, welcher CC zurückkehrt');
+ }
+
+ const { reactivateFree, reactivatePaid } = await import('./characters.js');
+ const note = `Forum-Antrag #${threadId}: ${thread.title}`;
+
+ // Ein unverändert existierender CC kommt gratis zurück; nur ein redesignter
+ // muss neu erschaffen werden. Welcher Fall vorliegt, entscheidet die
+ // bestehende Regel - hier wird sie nicht umgangen.
+ const target = await prisma.character.findUnique({ where: { id: input.characterId } });
+ if (!target) throw notFound('CC');
+
+ if (target.inactiveReason === 'REWORKED') {
+ const result = await reactivatePaid(
+ input.characterId,
+ input.useKind ?? 'REDESIGN_KULANZ',
+ ctx,
+ note,
+ );
+ characterId = result.character.id;
+ } else {
+ const result = await reactivateFree(input.characterId, ctx);
+ characterId = result.character.id;
+ }
+ } else if (
input.classification === ForumClassification.NEW_CC ||
input.classification === ForumClassification.REDESIGN
) {
@@ -313,14 +360,11 @@ export async function classifyThread(
if (input.predecessorId === undefined) {
throw badRequest('NO_PREDECESSOR', 'Bitte angeben, welcher CC redesignt wurde');
}
+ // Ein Redesign kann nicht aus dem CC-Topf bezahlt werden.
+ const redesignKind = input.useKind === 'REDESIGN_PAID' ? 'REDESIGN_PAID' : 'REDESIGN_KULANZ';
const result = await redesignCharacter(
input.predecessorId,
- {
- name,
- loreText: input.loreText,
- useKind: input.useKind ?? 'REDESIGN_KULANZ',
- note,
- },
+ { name, loreText: input.loreText, useKind: redesignKind, note },
ctx,
);
characterId = result.character.id;
@@ -352,3 +396,68 @@ export async function classifyThread(
return { characterId };
}
+
+/**
+ * Verknüpft einen von Hand angelegten CC mit seinem Forums-Thread und hakt
+ * diesen ab. Ohne das taucht derselbe Antrag beim nächsten Abgleich erneut in
+ * der Liste auf, obwohl er längst eingetragen ist.
+ */
+export async function linkThreadToCharacter(
+ threadId: number,
+ characterId: number,
+ classification: ForumClassification,
+ ctx: ActionContext,
+): Promise {
+ const character = await prisma.character.findUnique({
+ where: { id: characterId },
+ select: { id: true, name: true, playerId: true },
+ });
+ if (!character) throw notFound('CC');
+
+ const existing = await prisma.forumThread.findUnique({ where: { threadId } });
+
+ if (existing) {
+ if (existing.classification !== null) {
+ throw conflict(
+ 'ALREADY_CLASSIFIED',
+ `Dieser Thread ist bereits als "${existing.classification}" eingetragen`,
+ );
+ }
+ await prisma.forumThread.update({
+ where: { threadId },
+ data: {
+ classification,
+ needsClassification: false,
+ classifiedById: ctx.actorId,
+ classifiedAt: new Date(),
+ linkedCharacterId: characterId,
+ playerId: existing.playerId ?? character.playerId,
+ },
+ });
+ } else {
+ // Der Abgleich kennt den Thread noch nicht - trotzdem vormerken, damit er
+ // später nicht als offener Antrag auftaucht.
+ await prisma.forumThread.create({
+ data: {
+ threadId,
+ title: `Von Hand eingetragen: ${character.name}`,
+ url: `${env.FORUM_BASE_URL.replace(/\/+$/, '')}/index.php?thread/${threadId}/`,
+ playerId: character.playerId,
+ currentBoard: 'CUSTOM_CHARACTERS',
+ classification,
+ needsClassification: false,
+ classifiedById: ctx.actorId,
+ classifiedAt: new Date(),
+ linkedCharacterId: characterId,
+ history: { create: [{ board: 'CUSTOM_CHARACTERS' }] },
+ },
+ });
+ }
+
+ await recordAudit(prisma, ctx, {
+ action: 'forum.linked_manually',
+ targetType: 'forum_thread',
+ targetId: threadId,
+ details: { characterId, characterName: character.name, classification },
+ });
+}
diff --git a/server/test/fixtures/board.html b/server/test/fixtures/board.html
index 2bc1fbe..6d55be1 100644
--- a/server/test/fixtures/board.html
+++ b/server/test/fixtures/board.html
@@ -30,6 +30,33 @@
diff --git a/server/test/forum.test.ts b/server/test/forum.test.ts
index 2097045..3dc3902 100644
--- a/server/test/forum.test.ts
+++ b/server/test/forum.test.ts
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it } from 'node:test';
-import { parseBoard, parsePageCount } from '../src/lib/forum.js';
+import { hasIgnoredLabel, parseBoard, parsePageCount, parseThreadId } from '../src/lib/forum.js';
const here = dirname(fileURLToPath(import.meta.url));
const BOARD = readFileSync(join(here, 'fixtures', 'board.html'), 'utf8');
@@ -12,10 +12,10 @@ describe('Board-Übersicht lesen', () => {
const threads = parseBoard(BOARD);
it('liest die Threads der Liste', () => {
- assert.equal(threads.length, 3);
+ assert.equal(threads.length, 5);
assert.deepEqual(
threads.map((t) => t.threadId),
- [7001, 7002, 7003],
+ [7001, 7002, 7004, 7005, 7003],
);
});
@@ -49,3 +49,47 @@ describe('Board-Übersicht lesen', () => {
assert.deepEqual(parseBoard('nichts'), []);
});
});
+
+describe('Ankündigungen aussortieren', () => {
+ const threads = parseBoard(BOARD);
+
+ it('liest die Labels eines Threads', () => {
+ const regelwerk = threads.find((t) => t.threadId === 7004);
+ assert.deepEqual(regelwerk?.labels, ['Wichtig', 'Global']);
+ });
+
+ it('erkennt Wichtig und Information als Ausschluss', () => {
+ assert.equal(hasIgnoredLabel(threads.find((t) => t.threadId === 7004)!), true);
+ assert.equal(hasIgnoredLabel(threads.find((t) => t.threadId === 7005)!), true);
+ });
+
+ it('lässt echte Anträge durch, auch mit anderen Labels', () => {
+ assert.equal(hasIgnoredLabel(threads.find((t) => t.threadId === 7001)!), false);
+ });
+
+ it('wertet den Titel nicht aus', () => {
+ // "The Duke Rang Update" war ein echter Antrag, "Regelwerk" keiner. Am Titel
+ // ist das nicht zu erkennen - deshalb entscheidet allein das Label.
+ const ohneStichwort = parseBoard(BOARD).find((t) => t.threadId === 7005);
+ assert.ok(ohneStichwort, 'auch ein Thread ohne CC im Titel wird gelesen');
+ });
+});
+
+describe('Thread-Verweis lesen', () => {
+ it('nimmt den vollen Link', () => {
+ assert.equal(
+ parseThreadId('https://senseigaming.de/forum/index.php?thread/8041-cc-antrag-feldagent/'),
+ 8041,
+ );
+ });
+
+ it('nimmt auch nur den Pfad oder die blanke Nummer', () => {
+ assert.equal(parseThreadId('index.php?thread/7001-irgendwas/'), 7001);
+ assert.equal(parseThreadId('7001'), 7001);
+ });
+
+ it('weist Unbrauchbares ab', () => {
+ assert.equal(parseThreadId(''), null);
+ assert.equal(parseThreadId('kein link'), null);
+ });
+});
diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts
index 835753a..f95226f 100644
--- a/web/src/lib/format.ts
+++ b/web/src/lib/format.ts
@@ -57,6 +57,7 @@ export const BOARD_LABEL: Record = {
export const CLASSIFICATION_LABEL: Record = {
NEW_CC: 'Angenommen – neuer CC',
REDESIGN: 'Angenommen – Redesign',
+ REACTIVATION: 'Angenommen – Reaktivierung',
REJECTED: 'Abgelehnt',
IGNORE: 'Ignoriert',
};
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index a8842e7..b43c09b 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -193,7 +193,12 @@ export interface ImportRecord {
}
export type ForumBoard = 'CUSTOM_CHARACTERS' | 'AKTIVE_CCS' | 'CC_ARCHIV';
-export type ForumClassification = 'NEW_CC' | 'REDESIGN' | 'REJECTED' | 'IGNORE';
+export type ForumClassification =
+ | 'NEW_CC'
+ | 'REDESIGN'
+ | 'REACTIVATION'
+ | 'REJECTED'
+ | 'IGNORE';
export interface ForumThread {
threadId: number;
@@ -201,6 +206,7 @@ export interface ForumThread {
url: string;
authorName: string | null;
authorForumId: number | null;
+ labels: string | null;
playerId: number | null;
player: { id: number; displayName: string } | null;
currentBoard: ForumBoard;
@@ -221,7 +227,8 @@ export interface ForumScanSummary {
movedThreads: number;
boardChangeNotices: number;
skippedStubs: number;
+ skippedLabels: number;
+ skippedUnknownAuthor: number;
matchedPlayers: number;
- withoutPlayer: number;
failedBoards: { board: string; reason: string }[];
}
diff --git a/web/src/pages/Forum.tsx b/web/src/pages/Forum.tsx
index 7995d92..62d4d65 100644
--- a/web/src/pages/Forum.tsx
+++ b/web/src/pages/Forum.tsx
@@ -7,6 +7,7 @@ import { BOARD_LABEL, CLASSIFICATION_LABEL, formatDate } from '../lib/format';
import { useSession } from '../lib/session';
import type {
Character,
+ ForumClassification,
ForumScanSummary,
ForumThread,
PlayerDetail,
@@ -51,8 +52,11 @@ export default function Forum() {
const parts = [`${result.scannedPages} Seiten gelesen`];
if (result.newThreads > 0) parts.push(`${result.newThreads} neue Anträge`);
if (result.movedThreads > 0) parts.push(`${result.movedThreads} verschoben`);
- if (result.withoutPlayer > 0) parts.push(`${result.withoutPlayer} ohne Spieler`);
- if (result.skippedStubs > 0) parts.push(`${result.skippedStubs} Verweise übersprungen`);
+ if (result.skippedUnknownAuthor > 0) {
+ parts.push(`${result.skippedUnknownAuthor} ohne bekannten Spieler übersprungen`);
+ }
+ if (result.skippedLabels > 0) parts.push(`${result.skippedLabels} Ankündigungen`);
+ if (result.skippedStubs > 0) parts.push(`${result.skippedStubs} Verweise`);
if (result.failedBoards.length > 0) parts.push(`${result.failedBoards.length} Board-Fehler`);
setFlash(parts.join(', ') + '.');
await load();
@@ -80,7 +84,8 @@ export default function Forum() {
Forums-Anträge
Jeder Antrag wird von Hand entschieden — Titel und Board sind kein verlässliches
- Signal und lösen nie eine Buchung aus.
+ Signal und lösen nie eine Buchung aus. Aufgenommen werden nur Threads von Spielern,
+ die hier mit passendem Forum-Namen erfasst sind.