diff --git a/docs/superpowers/plans/2026-10-01-admin-umzug-etappe-2.md b/docs/superpowers/plans/2026-10-01-admin-umzug-etappe-2.md
new file mode 100644
index 0000000..6761a79
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-01-admin-umzug-etappe-2.md
@@ -0,0 +1,271 @@
+# 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 ` `/``/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:::`, 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
+
+### 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 bei `now >= exp`, fehlender/kaputter Signatur, anderer Länge – `timingSafeEqual` nur bei gleicher Länge).
+
+- [ ] **Step 1: Failing tests**
+
+```js
+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**
+
+```bash
+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 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//attachments/?exp=&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`)
+
+```js
+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**
+
+```bash
+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 })`. 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**
+
+```bash
+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//attachments`); `teamAuthHeaders() -> Promise<{ Authorization: string }>` (eigener `createJwtCache` über `account.createJWT`).
+- Produces: `loadChatComposer({ baseUrl, doc = globalThis.document, win = globalThis.window }) -> Promise`: hängt einmal ` ` und `