Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-01-admin-umzug-etappe-2.md
T
2026-10-01 23:10:48 +00:00

22 KiB
Raw Blame History

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 (Branch feature/admin-umzug, Stand 6718018 = origin/main). WOMS /home/webklar/apps/ticket.webklar.com/dev-admin-umzug (Branch feature/admin-umzug, Stand fce2f41 = origin/test). Vor Beginn je git fetch und git merge --ff-only origin/main bzw. origin/test.
  • Nie git add -A; nichts pushen (WOMS deployt jeden gepushten Branch). Livegang macht der Nutzer, Portal zuerst.
  • command grep -a statt grep. 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 über chat:<customerId>:<attachmentId>:<exp>, Gültigkeit 15 min, Query exp (ms seit Epoch) und sig.
  • 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.js showAdminChatNotice).
  • Speicherbremse für Builds/Playwright: systemd-run --scope -p MemoryMax=… -p MemorySwapMax=0.

Review Focus

  1. 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).
  2. 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).
  3. JWT nicht holbar beim Hochladen: Erwartung: Anhang zeigt Fehler, kein hängender Fortschritt (Task 3, Playwright).
  4. Kunden-Chat im Dashboard: ohne requestHeaders exakt wie bisher, kein Authorization-Header (Task 3, Playwright).
  5. Handy (390 px): Messenger zeigt Liste oder Gespräch mit Zurück-Knopf, kein waagerechtes Scrollen (Task 6, Playwright).

Portal

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 bei now >= exp, fehlender/kaputter Signatur, anderer Länge – timingSafeEqual nur 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, timingSafeEqual aus node:crypto; exp als 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"

Files:

  • Create: server/routes/chatAdmin.js (Chat-Teil aus server/routes/portalAdmin.js:309-474 verschoben, Hilfsfunktion customerNameMap mitnehmen 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) vor requireStaffJwt, router.use(chatAdminRouter) danach)
  • Test: test/teamChat.test.js

Interfaces:

  • Consumes: signFileLink, verifyFileLink (Task 1); requireStaffJwt (Etappe 1, setzt req.session mit appwriteUserId, name).

  • Produces (HTTP, unter /api/portal-admin und /api/team gleich): 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, Header X-File-Name …) → 201 { attachment }; DELETE /chats/:id/attachments/:att → 204; POST /chats/:id/read → { updated }.

  • Produces: in jeder Nachricht der Antworten von GET …/messages und POST …/messages trägt jeder Anhang zusätzlich url = ${config.publicBaseUrl}/api/team/chats/<customerId>/attachments/<id>?exp=<exp>&sig=<sig>.

  • Produces: GET /api/team/chats/:customerId/attachments/:id liefert die Datei mit gültiger Signatur ohne JWT; ungültige/abgelaufene Signatur → 403 { error: 'Link abgelaufen. Bitte die Unterhaltung neu laden.' }; ohne sig weiter zur JWT-Prüfung (ohne JWT 401, mit JWT 200).

  • Step 1: Failing tests (Aufbau wie test/teamRoutes.test.js; CHAT_UPLOAD_DIR auf mkdtempSync; setMailTransportForTests(mailSink()); Router /api/team und /api/portal-admin; Fixtures seedFixtures, 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.js eine Hilfe withSignedUrls(message, now) für die Antworten; files.js nutzt openAttachment({ viewer: 'admin' }) und sendAttachment aus services/chatAttachments.js.
  • Step 4: Run npm test → PASS (alle bisherigen Tests bleiben grün, insbesondere test/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 von npm 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/verwirft requestHeaders, wird der Anhang status: 'error' mit error = 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ür public/ plus Testseite /composer-test.html, die chat-composer.css/.js lädt und ChatComposer.create mit uploadUrl: '/upload' und einem per Query wählbaren requestHeaders aufruft; /upload per page.route)

Checks:

  • mit_header_upload: async requestHeaders → POST /upload trägt authorization: Bearer test.

  • mit_header_delete: Anhang entfernen → DELETE /upload/a1 trägt authorization: Bearer test.

  • ohne_option_kein_header: ohne requestHeaders → POST ohne authorization.

  • header_fehler: requestHeaders verwirft mit new 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.open asynchron holen; zwischendurch entfernte oder zerstörte Anhänge nicht mehr hochladen)

  • Step 4: Run node test/ui/composer-headers.ui.mjs → „alle Checks ok“; Run node 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: createJwtCache aus src/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 }> (eigener createJwtCache über account.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"> an doc.head; löst mit win.ChatComposer auf; Fehler → Error('Der Chat-Baustein konnte nicht geladen werden.'); je doc nur ein Ladevorgang (Promise wird wiederverwendet, nach Fehler neu versucht).

  • Step 1: Failing tests (Fake-doc mit head.appendChild, createElement → Objekt mit setAttribute, 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 aus buildNavLinks, 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); formatViewLogTime aus src/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 über key zu).

  • 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, sonst lastAt absteigend); 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 Absender senderName || '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-count nur bei count > 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/chat vor /customers/:id), src/lib/customerTabs.js (Reiter { id: 'chat', label: 'Chat' } vor protokoll), src/pages/CustomerDetailPage.jsx (Reiter rendert ChatThread)
  • Test: tests/unit/customerTabs.test.mjs (anpassen); Playwright-Harness im Scratchpad (nicht committen)

Interfaces:

  • Consumes: Tasks 4 und 5.

  • Produces: ChatThread({ customerId, customerName }): lädt loadChatComposer, rendert Nachrichten mit ChatComposer.renderMessage(msg, { mine: msg.sender === 'admin', meta: messageMeta(msg, customerName), fileUrl: (att) => att.url }) in einen scrollenden Verlauf, Eingabezeile mit ChatComposer.create({ root, dropZone, uploadUrl: chatUploadUrl(id), requestHeaders: teamAuthHeaders, placeholder: 'Antwort an <Name>…', placeholderShort: 'Antwort…', send, onSent }); Hinweiszeile mit noticeText; Abfrage alle 15 s mit since; neue Kundennachrichten → markChatRead; beim Öffnen markChatRead; vollständiges Neuladen nach needsFullRefresh (Intervall und beim Sichtbarwerden); Antworten für einen inzwischen gewechselten Kunden werden verworfen; composer.destroy() beim Wechsel/Verlassen; Ladefehler → Zeile mit „Erneut versuchen“.

  • Produces: CustomerChatPage unter /customers/chat: links Feld „Neue Nachricht an Kunde…“ (AutocompleteInput über useCustomers, Treffer per Name → ?kunde=<id>) und Liste (sortChats, Zahl ungelesen, lastLine, Zeit), Abfrage alle 30 s; rechts ChatThread zu ?kunde oder „Unterhaltung links auswählen“. Unter 720 px: ohne kunde nur Liste, mit kunde nur 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.css setzt --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 mit systemd-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/* per ctx.route, chat-composer.js/.css aus 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 signierter url; Datei hochladen → POST an /api/team/chats/c1/attachments mit 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/chats ohne JWT 401, chat-composer.js enthält requestHeaders, Admin-Panel-Chat lädt; 3. WOMS git -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.