Files
Webklar-Kundenbereich/scripts/setup-chat-collection.mjs
T
ClaudeandClaude Opus 5.5 35d98da977 Chat: Bilder, Kamerafoto, Dateien und Sprachnachrichten
- Kundenbereich und Admin-Chat: Bueroklammer-Menue (Bild, Kamera, Datei),
  Mikrofon fuer Sprachnachrichten, Vorschau vor dem Senden mit
  Upload-Fortschritt, Einfuegen (Strg+V) und Hineinziehen von Dateien
- Kamera: am Handy/Tablet die Kamera-App, am Rechner die Kamera im Browser
  mit Vorschau, Kamerawechsel und "Nochmal"
- Sprachnachricht: Aufnahme mit Zeit und Pegel, vorher anhoerbar oder
  direkt senden, hoechstens 5 Minuten
- Verlauf: Bilder (einzeln bzw. Raster, Grossansicht), Player fuer
  Sprachnachrichten, Datei-Kacheln mit Download
- Server: jede Datei einzeln roh hochgeladen (max. 25 MB, 10 je Nachricht,
  max. 3 gleichzeitig), Typ aus dem Inhalt, nur erlaubte Endungen, keine
  Programme; Ablage unter upload-data/chat/<Kunde>/ ausserhalb von public/,
  Auslieferung nur mit Login; Bilder, Audio und PDF inline, alles andere
  als Download (nosniff, CSP sandbox)
- Nicht gesendete Anhaenge werden nach 24 Stunden geloescht
- Benachrichtigungs-Mail nennt nur Art und Namen der Anhaenge
- Appwrite: neues Attribut portalMessages.attachments
  (scripts/setup-chat-collection.mjs, vor dem Deploy ausfuehren)
- Datenschutzerklaerung Abschnitt 6 ergaenzt (Stand 26.09.2026), PDF neu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:01:17 +00:00

115 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Legt die Collection portalMessages (Kunde-Admin-Chat) in woms-database an.
* Idempotent: existierende Collection/Attribute/Indexe werden uebersprungen
* (also auch zum Nachruesten neuer Attribute wie `attachments` geeignet).
* Usage: APPWRITE_API_KEY=... node scripts/setup-chat-collection.mjs
*/
const endpoint = (process.env.APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1').replace(/\/$/, '')
const projectId = process.env.APPWRITE_PROJECT_ID || '6a1058610003c5a13a05'
const apiKey = process.env.APPWRITE_API_KEY
const databaseId = process.env.APPWRITE_DATABASE_ID || 'woms-database'
const COLLECTION_ID = 'portalMessages'
if (!apiKey) {
console.error('APPWRITE_API_KEY erforderlich')
process.exit(1)
}
async function aw(path, { method = 'GET', body } = {}) {
const response = await fetch(`${endpoint}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Appwrite-Project': projectId,
'X-Appwrite-Key': apiKey,
},
body: body ? JSON.stringify(body) : undefined,
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
const err = new Error(data.message || `Appwrite ${response.status}`)
err.status = response.status
err.type = data.type
throw err
}
return data
}
async function ensure(label, fn) {
try {
await fn()
console.log(`+ ${label}`)
} catch (e) {
if (e.status === 409 || /already exists/i.test(e.message)) {
console.log(`= ${label} (existiert)`)
} else {
throw new Error(`${label}: ${e.message}`)
}
}
}
async function waitForAttribute(key, maxSec = 30) {
for (let i = 0; i < maxSec; i += 1) {
const attr = await aw(
`/databases/${databaseId}/collections/${COLLECTION_ID}/attributes/${key}`
).catch(() => null)
if (attr?.status === 'available') return
await new Promise((r) => setTimeout(r, 1000))
}
throw new Error(`Attribut ${key} wurde nicht verfuegbar`)
}
const base = `/databases/${databaseId}/collections`
await ensure(`Collection ${COLLECTION_ID}`, () =>
aw(base, {
method: 'POST',
body: {
collectionId: COLLECTION_ID,
name: 'Portal Messages (Kunde-Admin-Chat)',
documentSecurity: false,
permissions: [],
},
})
)
const attributes = [
{ kind: 'string', key: 'customerId', size: 64, required: true },
{ kind: 'string', key: 'sender', size: 16, required: true },
{ kind: 'string', key: 'senderName', size: 128, required: false },
{ kind: 'string', key: 'text', size: 4000, required: true },
// JSON-Liste der Anhaenge (Bilder, Dateien, Sprachnachrichten); Dateien selbst liegen auf dem Server
{ kind: 'string', key: 'attachments', size: 20000, required: false },
{ kind: 'boolean', key: 'readByAdmin', required: false },
{ kind: 'boolean', key: 'readByCustomer', required: false },
{ kind: 'datetime', key: 'createdAt', required: true },
]
for (const attr of attributes) {
const { kind, ...body } = attr
await ensure(`Attribut ${attr.key}`, () =>
aw(`${base}/${COLLECTION_ID}/attributes/${kind}`, { method: 'POST', body })
)
}
for (const attr of attributes) {
await waitForAttribute(attr.key)
}
console.log('Alle Attribute verfuegbar.')
const indexes = [
{ key: 'idx_customer_created', type: 'key', attributes: ['customerId', 'createdAt'], orders: ['ASC', 'DESC'] },
{ key: 'idx_created', type: 'key', attributes: ['createdAt'], orders: ['DESC'] },
{ key: 'idx_read_admin', type: 'key', attributes: ['readByAdmin', 'sender'], orders: ['ASC', 'ASC'] },
{ key: 'idx_customer_read', type: 'key', attributes: ['customerId', 'readByCustomer'], orders: ['ASC', 'ASC'] },
]
for (const idx of indexes) {
await ensure(`Index ${idx.key}`, () =>
aw(`${base}/${COLLECTION_ID}/indexes`, { method: 'POST', body: idx })
)
}
console.log('Fertig.')