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>
112 lines
3.5 KiB
JavaScript
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.')
|