22 KiB
Admin-Panel ins Ticketsystem – Etappe 2 (Chat) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Mitarbeiter chatten mit Kunden im WOMS – Messenger unter Kunden → Chat, Reiter „Chat“ auf der Kundenseite, Zahl der ungelesenen Nachrichten in der Sidebar – mit Anhängen wie heute; das Admin-Panel bleibt nutzbar.
Architecture: Die Chat-Routen des Admin-Panels werden ein eigener Router, den das Portal unter /api/portal-admin (Cookie, alt) und /api/team (JWT, neu) einhängt. Anhänge bekommen signierte Links (15 min), damit <img>/<audio>/Download im WOMS ohne Header funktionieren. WOMS lädt chat-composer.js/.css zur Laufzeit vom Portal (eine Quelle) und bettet Eingabezeile und Sprechblasen in React ein; der Composer bekommt dafür die Option requestHeaders.
Tech Stack: Portal: Express 4 (ESM, Node 20), node:test + test/support/*, Playwright-core für UI-Checks. WOMS: React 18, react-router 6, appwrite 13, node:test + tests/unit/load.mjs.
Spec: docs/superpowers/specs/2026-10-01-admin-panel-ins-ticketsystem-design.md (Abschnitt 1 Sidebar, Abschnitt 2 „Chat“, Abschnitt 4 Tabelle /api/team Zeile Chat + „Dateien ohne Header“ + CORS, Etappe 2 in Abschnitt 5)
Global Constraints
- Arbeitsorte: Portal
/home/webklar/apps/kundenbereich.webklar.com/dev-admin-umzug(Branchfeature/admin-umzug, Stand6718018= origin/main). WOMS/home/webklar/apps/ticket.webklar.com/dev-admin-umzug(Branchfeature/admin-umzug, Standfce2f41= origin/test). Vor Beginn jegit fetchundgit merge --ff-only origin/mainbzw.origin/test. - Nie
git add -A; nichts pushen (WOMS deployt jeden gepushten Branch). Livegang macht der Nutzer, Portal zuerst. command grep -astattgrep. UI-Texte deutsch mit Umlauten.- Funktionsgleich zum Admin-Panel, keine neuen Chat-Funktionen. Kunden-Chat im Dashboard bleibt unverändert.
- Signierte Links: HMAC-SHA256 (hex) mit
config.sessionSecretüberchat:<customerId>:<attachmentId>:<exp>, Gültigkeit 15 min, Queryexp(ms seit Epoch) undsig. - Polling: offenes Gespräch 15 s, Gesprächsliste 30 s, Sidebar-Zahl 60 s – jeweils nur bei sichtbarem Tab. Gesprächsverlauf komplett neu laden (frische Links) nach 10 min.
- K17-Hinweistexte wortgleich zum Admin-Panel (
public/admin.jsshowAdminChatNotice). - Speicherbremse für Builds/Playwright:
systemd-run --scope -p MemoryMax=… -p MemorySwapMax=0.
Review Focus
- Gespräch länger als 15 min offen: alte Anhang-Links laufen ab. Erwartung: Verlauf wird nach 10 min bzw. beim Zurückkehren in den Tab neu geladen, Download/Lightbox funktionieren weiter (Task 5:
needsFullRefresh). - Schneller Kundenwechsel im Messenger: Antwort für Kunde A kommt an, nachdem B gewählt wurde. Erwartung: nichts von A erscheint im Gespräch von B (Task 6, Playwright).
- JWT nicht holbar beim Hochladen: Erwartung: Anhang zeigt Fehler, kein hängender Fortschritt (Task 3, Playwright).
- Kunden-Chat im Dashboard: ohne
requestHeadersexakt wie bisher, kein Authorization-Header (Task 3, Playwright). - Handy (390 px): Messenger zeigt Liste oder Gespräch mit Zurück-Knopf, kein waagerechtes Scrollen (Task 6, Playwright).
Portal
Task 1: Signierte Datei-Links
Files:
- Create:
server/services/signedLinks.js - Test:
test/signedLinks.test.js
Interfaces:
-
Produces:
FILE_LINK_TTL_MS = 900000;signFileLink({ kind, customerId, id, now = Date.now() }) -> { exp: number, sig: string };verifyFileLink({ kind, customerId, id, exp, sig, now = Date.now() }) -> boolean(falsch beinow >= exp, fehlender/kaputter Signatur, anderer Länge –timingSafeEqualnur bei gleicher Länge). -
Step 1: Failing tests
test('Signatur passt nur fuer dieselbe Datei, denselben Kunden und dieselbe Art', () => {
const { exp, sig } = signFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', now: 1000 })
assert.equal(exp, 1000 + 900000)
assert.match(sig, /^[0-9a-f]{64}$/)
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: 2000 }), true)
for (const other of [{ customerId: 'c2' }, { id: 'a2' }, { kind: 'takeover' }]) {
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: 2000, ...other }), false)
}
})
test('abgelaufen genau bei exp, manipuliertes exp, kaputte Signatur', () => {
const { exp, sig } = signFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', now: 0 })
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: exp }), false)
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp: exp + 1, sig, now: 0 }), false)
for (const bad of ['', 'abc', 'x'.repeat(64), undefined]) {
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig: bad, now: 0 }), false)
}
})
- Step 2: Run
node --test test/signedLinks.test.js→ FAIL (Modul fehlt) - Step 3: Implementieren (
createHmac,timingSafeEqualausnode:crypto;expals Zahl prüfen) - Step 4: Run
node --test test/signedLinks.test.js→ PASS - Step 5: Commit
git add server/services/signedLinks.js test/signedLinks.test.js
git commit -m "Signierte Datei-Links (15 min) fuer Anhaenge im Ticketsystem"
Task 2: Chat-Router für Admin-Panel und Team-Eingang, signierte Anhang-Links
Files:
- Create:
server/routes/chatAdmin.js(Chat-Teil ausserver/routes/portalAdmin.js:309-474verschoben, HilfsfunktioncustomerNameMapmitnehmen bzw. dort exportieren) - Modify:
server/routes/portalAdmin.js(Chat-Routen entfernen,router.use(chatAdminRouter)) - Create:
server/routes/team/files.js - Modify:
server/routes/team/index.js(router.use(teamFilesRouter)vorrequireStaffJwt,router.use(chatAdminRouter)danach) - Test:
test/teamChat.test.js
Interfaces:
-
Consumes:
signFileLink,verifyFileLink(Task 1);requireStaffJwt(Etappe 1, setztreq.sessionmitappwriteUserId,name). -
Produces (HTTP, unter
/api/portal-adminund/api/teamgleich):GET /chats→{ chats: [{ customerId, customerName, lastText, lastSender, lastAt, unreadFromCustomer }] };GET /chats/unread-count→{ unread };GET /chats/:id/messages?since=→{ messages };POST /chats/:id/messages{ text, attachmentIds }→ 201{ message, notice };POST /chats/:id/attachments(roh, HeaderX-File-Name…) → 201{ attachment };DELETE /chats/:id/attachments/:att→ 204;POST /chats/:id/read→{ updated }. -
Produces: in jeder Nachricht der Antworten von
GET …/messagesundPOST …/messagesträgt jeder Anhang zusätzlichurl=${config.publicBaseUrl}/api/team/chats/<customerId>/attachments/<id>?exp=<exp>&sig=<sig>. -
Produces:
GET /api/team/chats/:customerId/attachments/:idliefert die Datei mit gültiger Signatur ohne JWT; ungültige/abgelaufene Signatur → 403{ error: 'Link abgelaufen. Bitte die Unterhaltung neu laden.' }; ohnesigweiter zur JWT-Prüfung (ohne JWT 401, mit JWT 200). -
Step 1: Failing tests (Aufbau wie
test/teamRoutes.test.js;CHAT_UPLOAD_DIRaufmkdtempSync;setMailTransportForTests(mailSink()); Router/api/teamund/api/portal-admin; FixturesseedFixtures,adminCookie)
test('Liste und ungelesene Zahl ueber den Team-Eingang', async () => { /* 2 Kundennachrichten c1 unread -> chats[0].unreadFromCustomer 2, unread-count 2 */ })
test('Senden als Mitarbeiter: Absendername aus dem JWT, K17-Mail an den Kunden', async () => {
const res = await post('/api/team/chats/c1/messages', { text: 'Hallo' }, staff)
assert.equal(res.status, 201)
assert.equal(res.body.message.senderName, 'Andrej')
assert.equal(res.body.notice.sent, true)
assert.equal(sink.sent.at(-1).to[0].address, 'k@example.invalid')
})
test('Anhang hochladen, mit Nachricht senden -> signierte url', async () => {
// POST /api/team/chats/c1/attachments, Body Buffer 'hallo', Header content-type application/octet-stream, x-file-name notiz.txt
// POST messages { text: '', attachmentIds: [id] } -> message.attachments[0].url passt auf
// /^https:\/\/project\.webklar\.com\/api\/team\/chats\/c1\/attachments\/[^?]+\?exp=\d+&sig=[0-9a-f]{64}$/
})
test('signierter Link: ohne JWT 200 mit Inhalt, manipuliert/fremder Kunde/abgelaufen 403, ohne sig 401, JWT ohne sig 200', async (t) => {})
test('Lesen markiert Kundennachrichten', async () => { /* POST /api/team/chats/c1/read -> updated 2, danach unread-count 0 */ })
test('Admin-Panel unveraendert: /api/portal-admin/chats mit Admin-Cookie 200, ohne 401', async () => {})
- Step 2: Run
node --test test/teamChat.test.js→ FAIL (404 unter/api/team/chats) - Step 3: Implementieren. In
chatAdmin.jseine HilfewithSignedUrls(message, now)für die Antworten;files.jsnutztopenAttachment({ viewer: 'admin' })undsendAttachmentausservices/chatAttachments.js. - Step 4: Run
npm test→ PASS (alle bisherigen Tests bleiben grün, insbesonderetest/support.test.js,test/chatNotice.test.js) - Step 5: Commit
git add server/routes/chatAdmin.js server/routes/portalAdmin.js server/routes/team/files.js server/routes/team/index.js test/teamChat.test.js
git commit -m "Chat ueber den Team-Eingang: gemeinsamer Router fuer Admin-Panel und Ticketsystem, signierte Anhang-Links"
Task 3: Composer-Option requestHeaders
Files:
- Modify:
public/chat-composer.js(Optionsdoku Zeile ~536;deleteOnServer~711;upload~734) - Create:
test/ui/composer-headers.ui.mjs(Playwright, nicht Teil vonnpm test)
Interfaces:
-
Produces:
ChatComposer.create({ …, requestHeaders?: () => object | Promise<object> }). Die Header gehen an den Upload-XHR (zusätzlich zu den bestehenden) und an das DELETE beim Entfernen. Wirft/verwirftrequestHeaders, wird der Anhangstatus: 'error'miterror = err.message || 'Anmeldung abgelaufen. Bitte Seite neu laden.'. Ohne Option: Verhalten unverändert. -
Step 1: UI-Check schreiben (Muster
test/ui/kundenansicht.ui.mjs; statischer Server fürpublic/plus Testseite/composer-test.html, diechat-composer.css/.jslädt undChatComposer.createmituploadUrl: '/upload'und einem per Query wählbarenrequestHeadersaufruft;/uploadperpage.route)
Checks:
-
mit_header_upload: asyncrequestHeaders→ POST/uploadträgtauthorization: Bearer test. -
mit_header_delete: Anhang entfernen → DELETE/upload/a1trägtauthorization: Bearer test. -
ohne_option_kein_header: ohnerequestHeaders→ POST ohneauthorization. -
header_fehler:requestHeadersverwirft mitnew Error('JWT weg')→ Anhang zeigt „JWT weg“, kein POST. -
Step 2: Run
node test/ui/composer-headers.ui.mjs→ FAIL (mit_header_upload,mit_header_delete,header_fehler) -
Step 3: Implementieren (Header vor
xhr.openasynchron holen; zwischendurch entfernte oder zerstörte Anhänge nicht mehr hochladen) -
Step 4: Run
node test/ui/composer-headers.ui.mjs→ „alle Checks ok“; Runnode test/ui/kundenansicht.ui.mjs→ „alle Checks ok“ -
Step 5: Commit
git add public/chat-composer.js test/ui/composer-headers.ui.mjs
git commit -m "Chat-Baustein: Option requestHeaders fuer Hochladen und Entfernen (Ticketsystem mit JWT)"
WOMS
Task 4: Team-API für den Chat und Laden des Chat-Bausteins
Files:
- Modify:
src/lib/teamApi.js - Create:
src/lib/loadChatComposer.js - Test:
tests/unit/loadChatComposer.test.mjs
Interfaces:
-
Consumes:
createJwtCacheaussrc/lib/serverpulsAuth.js; HTTP aus Task 2. -
Produces in
teamApi.js:PORTAL_URL(export);listChats() -> Promise<{ chats }>;getChatUnreadCount() -> Promise<{ unread }>;getChatMessages(customerId, since = '') -> Promise<{ messages }>;sendChatMessage(customerId, { text, attachmentIds }) -> Promise<{ message, notice }>;markChatRead(customerId) -> Promise<{ updated }>;chatUploadUrl(customerId) -> string(${PORTAL_URL}/api/team/chats/<id>/attachments);teamAuthHeaders() -> Promise<{ Authorization: string }>(eigenercreateJwtCacheüberaccount.createJWT). -
Produces:
loadChatComposer({ baseUrl, doc = globalThis.document, win = globalThis.window }) -> Promise<ChatComposer>: hängt einmal<link rel="stylesheet" href="<baseUrl>/chat-composer.css">und<script src="<baseUrl>/chat-composer.js">andoc.head; löst mitwin.ChatComposerauf; Fehler →Error('Der Chat-Baustein konnte nicht geladen werden.'); jedocnur ein Ladevorgang (Promise wird wiederverwendet, nach Fehler neu versucht). -
Step 1: Failing tests (Fake-
docmithead.appendChild,createElement→ Objekt mitsetAttribute,onload/onerror)
test('haengt CSS und Skript einmal an und liefert window.ChatComposer', async () => { /* zwei Aufrufe -> 2 Elemente, gleicher Rueckgabewert */ })
test('Skript laedt nicht -> deutsche Meldung, naechster Aufruf versucht es neu', async () => {})
test('schon geladen (win.ChatComposer vorhanden) -> nichts anhaengen', async () => {})
- Step 2: Run
node --test tests/unit/loadChatComposer.test.mjs→ FAIL - Step 3: Implementieren
- Step 4: Run
npm test→ PASS - Step 5: Commit
git add src/lib/teamApi.js src/lib/loadChatComposer.js tests/unit/loadChatComposer.test.mjs
git commit -m "Chat: Team-API und Laden des Chat-Bausteins vom Kundenportal"
Task 5: Sidebar mit Unterpunkt „Chat“ und Zahl, Chat-Hilfen
Files:
- Create:
src/lib/navLinks.js,src/lib/chatFormat.js,src/hooks/useChatUnread.js - Modify:
src/components/Navbar.jsx:57-113(Liste ausbuildNavLinks, Unterpunkte rendern),src/styles/global.css(Unterpunkt-Stil.side-sublink, Zahl.side-count) - Test:
tests/unit/navLinks.test.mjs,tests/unit/chatFormat.test.mjs
Interfaces:
-
Consumes:
getChatUnreadCount(Task 4);formatViewLogTimeaussrc/lib/viewLog.js. -
Produces:
buildNavLinks({ isAdmin, pulseLevel, chatUnread }) -> Array<{ key, label, href, also?, paused?, dot?, count?, children? }>; Kunden-Eintrag:{ key: 'customers', label: 'Kunden', href: '/customers', children: [{ key: 'chat', label: 'Chat', href: '/customers/chat', count: chatUnread }] }; übrige Einträge und Reihenfolge wie heute (Navbar ordnet Icons überkeyzu). -
Produces:
useChatUnread() -> number(alle 60 s bei sichtbarem Tab, sofort beim Sichtbarwerden; Fehler behalten den letzten Wert). -
Produces in
chatFormat.js:sortChats(chats)(ungelesene zuerst, sonstlastAtabsteigend);noticeText(notice, time)mit den drei Texten aus dem Admin-Panel (E-Mail-Hinweis an ${to} gesendet (${time})/Kein neuer E-Mail-Hinweis: Der Kunde wurde in den letzten 15 Minuten schon benachrichtigt./Kein E-Mail-Hinweis: ${reason}; ohne notice'');lastLine(chat)(Du:vor eigenen);messageMeta(msg, customerName)(<Absender> · dd.mm.yyyy hh:mm, eigener AbsendersenderName || 'Team');needsFullRefresh(lastFullAt, now) -> boolean(ab 600000 ms). -
Step 1: Failing tests
test('buildNavLinks: Chat unter Kunden mit Zahl, Admin-Eintraege nur fuer Admins', () => {
const links = buildNavLinks({ isAdmin: false, pulseLevel: 'ok', chatUnread: 3 })
const kunden = links.find((l) => l.key === 'customers')
assert.deepEqual(kunden.children, [{ key: 'chat', label: 'Chat', href: '/customers/chat', count: 3 }])
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'legal'])
assert.ok(buildNavLinks({ isAdmin: true, pulseLevel: 'warn', chatUnread: 0 }).some((l) => l.key === 'serverpuls' && l.dot === 'warn'))
})
test('sortChats: ungelesene zuerst, dann neueste', () => {})
test('noticeText: gesendet, gedrosselt, Grund, leer', () => {
assert.equal(noticeText({ sent: true, to: 'k@x.de' }, '14:05'), 'E-Mail-Hinweis an k@x.de gesendet (14:05)')
assert.equal(noticeText({ sent: false, reason: 'throttled' }, '14:05'), 'Kein neuer E-Mail-Hinweis: Der Kunde wurde in den letzten 15 Minuten schon benachrichtigt.')
assert.equal(noticeText({ sent: false, reason: 'SMTP weg' }, '14:05'), 'Kein E-Mail-Hinweis: SMTP weg')
assert.equal(noticeText(null, '14:05'), '')
})
test('messageMeta und lastLine', () => {})
test('needsFullRefresh ab 10 Minuten', () => {
assert.equal(needsFullRefresh(0, 599999), false)
assert.equal(needsFullRefresh(0, 600000), true)
})
- Step 2: Run
node --test tests/unit/navLinks.test.mjs tests/unit/chatFormat.test.mjs→ FAIL - Step 3: Implementieren; Navbar: Unterpunkte eingerückt (
.side-sublink), eingeklappt nur Icon (IconMessages), Zahl als.side-countnur beicount > 0; „Kunden“ bleibt markiert, solange ein Unterpunkt aktiv ist. Knopf „Projekt-Admin“ bleibt (fällt erst in Etappe 4). - Step 4: Run
npm test→ PASS - Step 5: Commit
git add src/lib/navLinks.js src/lib/chatFormat.js src/hooks/useChatUnread.js src/components/Navbar.jsx src/styles/global.css tests/unit/navLinks.test.mjs tests/unit/chatFormat.test.mjs
git commit -m "Sidebar: Chat als Unterpunkt von Kunden mit Zahl der ungelesenen Nachrichten"
Task 6: Gespräch, Messenger und Reiter „Chat“
Files:
- Create:
src/components/chat/ChatThread.jsx,src/pages/CustomerChatPage.jsx,src/styles/chat.css - Modify:
src/App.jsx:51(Route/customers/chatvor/customers/:id),src/lib/customerTabs.js(Reiter{ id: 'chat', label: 'Chat' }vorprotokoll),src/pages/CustomerDetailPage.jsx(Reiter rendertChatThread) - Test:
tests/unit/customerTabs.test.mjs(anpassen); Playwright-Harness im Scratchpad (nicht committen)
Interfaces:
-
Consumes: Tasks 4 und 5.
-
Produces:
ChatThread({ customerId, customerName }): lädtloadChatComposer, rendert Nachrichten mitChatComposer.renderMessage(msg, { mine: msg.sender === 'admin', meta: messageMeta(msg, customerName), fileUrl: (att) => att.url })in einen scrollenden Verlauf, Eingabezeile mitChatComposer.create({ root, dropZone, uploadUrl: chatUploadUrl(id), requestHeaders: teamAuthHeaders, placeholder: 'Antwort an <Name>…', placeholderShort: 'Antwort…', send, onSent }); Hinweiszeile mitnoticeText; Abfrage alle 15 s mitsince; neue Kundennachrichten →markChatRead; beim ÖffnenmarkChatRead; vollständiges Neuladen nachneedsFullRefresh(Intervall und beim Sichtbarwerden); Antworten für einen inzwischen gewechselten Kunden werden verworfen;composer.destroy()beim Wechsel/Verlassen; Ladefehler → Zeile mit „Erneut versuchen“. -
Produces:
CustomerChatPageunter/customers/chat: links Feld „Neue Nachricht an Kunde…“ (AutocompleteInputüberuseCustomers, Treffer per Name →?kunde=<id>) und Liste (sortChats, Zahl ungelesen,lastLine, Zeit), Abfrage alle 30 s; rechtsChatThreadzu?kundeoder „Unterhaltung links auswählen“. Unter 720 px: ohnekundenur Liste, mitkundenur Gespräch mit Knopf „← Alle Unterhaltungen“. -
Produces: Reihenfolge der Kundenreiter
Übersicht · Tickets · Rechnungen · Projekte · Chat · Protokoll(Rechnungen nur Admins). -
Step 1: Test anpassen
assert.deepEqual(customerTabs(false).map((t) => t.id), ['overview', 'tickets', 'projects', 'chat', 'protokoll'])
assert.equal(resolveCustomerTab('chat', false), 'chat')
- Step 2: Run
node --test tests/unit/customerTabs.test.mjs→ FAIL - Step 3: Implementieren;
chat.csssetzt--chat-accent: var(--accent),--chat-accent-hover: var(--accent-2),--chat-accent-soft: var(--accent-soft)und die Sprechblasen/Verlauf-Stile (.chat-messages,.chat-bubble.mine/.theirs,.chat-meta) innerhalb.woms-chat. - Step 4: Run
npm test→ PASS; Vite-Build mitsystemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 node node_modules/vite/bin/vite.js build --outDir <scratchpad>/woms-out --emptyOutDir→ „built in“ - Step 5: Playwright-Harness (wie Etappe 1: esbuild-Bündel mit AuthContext-Stub, Appwrite-REST und
https://project.webklar.com/api/team/*perctx.route,chat-composer.js/.cssaus dem Portal-Worktree ausgeliefert). Checks: Messenger-Liste sortiert, Zahl sichtbar; Klick öffnet Gespräch, Adresse?kunde=c1; Nachricht senden → POST mit Bearer, Hinweiszeile „E-Mail-Hinweis an … gesendet (hh:mm)“; Bild-Anhang in der Nachricht →<img>mit signierterurl; Datei hochladen → POST an/api/team/chats/c1/attachmentsmit Bearer; Kunde wechseln, während die Antwort für A verzögert ist → keine Blase von A im Gespräch von B; Reiter „Chat“ auf der Kundenseite zeigt dasselbe Gespräch; Sidebar-Zahl; 390 px: Liste ↔ Gespräch mit Zurück-Knopf, kein waagerechtes Scrollen; keine JS-Fehler. Screenshots nach/root/reports/admin-umzug-ui/woms-chat-*.png. - Step 6: Commit
git add src/components/chat/ChatThread.jsx src/pages/CustomerChatPage.jsx src/styles/chat.css src/App.jsx src/lib/customerTabs.js src/pages/CustomerDetailPage.jsx tests/unit/customerTabs.test.mjs
git commit -m "Chat im Ticketsystem: Messenger unter Kunden, Reiter Chat auf der Kundenseite"
Abschluss
Task 7: Gesamtprüfung und Übergabe
- Portal
npm test→ grün; neue Tests unter Node 20 (docker run --rm … node:20-alpine node --test test/signedLinks.test.js test/teamChat.test.js) → grün. - WOMS
npm test→ grün; Build aus Task 6 erfolgreich. - Abschlussreview beider Branches (frischer Reviewer, Modell sonnet).
- Übergabe mit Livegang: 1. Portal
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-admin-umzug push origin feature/admin-umzug:main; 2. Live-Check (ich):/api/team/chatsohne JWT 401,chat-composer.jsenthältrequestHeaders, Admin-Panel-Chat lädt; 3. WOMSgit -C /home/webklar/apps/ticket.webklar.com/dev-admin-umzug push origin feature/admin-umzug:test; 4. Live-Check (ich): Bundle enthält „Neue Nachricht an Kunde“; 5. Nutzer-Test: Nachricht aus dem WOMS senden, Antwort des Kunden mit Bild ansehen.