Files
Webklar-Kundenbereich/scripts/setup-chat-collection.mjs
Kenso Grimm d4bd510dcd feat: Kundenportal-Ausbau - Tabs, Projekt-Einblicke, Git-Zeiterfassung, Rechnungen, Chat
Kunden-Dashboard:
- Tab-Navigation: Projekte | Was wurde gemacht | Rechnungen | Chat
- Projekt-Details pro Projekt: letzte Git-Commits (Titel+Beschreibung),
  Projektgroesse, Datei-Uebersicht (Top-Level aggregiert), Ticket-Arbeiten
- Rechnungen: gestylte Liste mit Status-Pillen, Ansehen/Zahlen-Link,
  PDF-Download ueber Server-Proxy (IDOR-geschuetzt)
- Chat mit dem Webklar-Team (Polling, Ungelesen-Badge, viewAs blockiert)

Admin-Dashboard:
- Chat-Tab: Konversationsliste + Thread + Antwortfeld, Ungelesen-Badge

Backend:
- giteaAdmin: getRepoInfo, listRepoCommits, getRepoTreeSummary
- projectInsights: 5-min-Cache, Invalidierung per Gitea-Webhook
- /api/projects/:id/git|files|work mit Ownership-Check (404)
- /api/chat/* (Kunde) + /api/portal-admin/chats/* (Admin), portalMessages-Collection
- mailer: E-Mail-Benachrichtigung an Admins bei Kundennachricht (15-min-Throttle)
- gitWorksheet: dd.mm.yyyy, voller Commit-Body, startTime leer (Zeit-Nachtrag),
  Auto-Webpage-Ticket bei Push auf Projekt ohne Ticket (ensureProjectTicket)
- customerActivity: Git-Push-Eintraege fuer Kunden sichtbar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:01:24 +00:00

112 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Legt die Collection portalMessages (Kunde-Admin-Chat) in woms-database an.
* Idempotent: existierende Collection/Attribute/Indexe werden uebersprungen.
* 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 },
{ 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.')