feat(pm): add bug-report button with screenshot annotation and admin panel

- New BugReport MongoDB model (comment, contact, userId, screenshot, pmState, status)
- POST /api/bug-reports — authenticated submit; emails emanuel@faktorxmensch.com on receipt
- GET/PATCH /api/admin/bug-reports + /[id] — admin list, detail with screenshot, status toggle
- /pm: "Bug" button in HUD captures viewport screenshot (html2canvas), shows annotation
  canvas where testers can draw arrows; submits comment + contact + state snapshot
- /admin: new "Bug Reports" tab with open-count badge, screenshot expand, "Erledigt" toggle,
  and "In /pm öffnen" link that restores captured engine state via ?restoreBugReport=<id>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-06-18 10:05:25 +02:00
parent bfca1832a8
commit ccab0e7116
10 changed files with 819 additions and 2 deletions

View File

@@ -0,0 +1,27 @@
import { defineEventHandler, getRouterParam, createError } from 'h3'
import { requireAdmin } from '../../../utils/auth'
import { BugReport } from '../../../models/BugReport'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
const id = getRouterParam(event, 'id')
const doc = await BugReport.findById(id).populate('userId', 'email name role').lean() as any
if (!doc) {
throw createError({ statusCode: 404, statusMessage: 'Bug report not found' })
}
return {
id: String(doc._id),
comment: doc.comment,
contact: doc.contact,
user: doc.userId
? { id: String((doc.userId as any)._id), email: (doc.userId as any).email, name: (doc.userId as any).name }
: undefined,
screenshot: doc.screenshot || null,
pmState: doc.pmState || null,
status: doc.status,
createdAt: doc.createdAt ? new Date(doc.createdAt).toISOString() : null,
}
})

View File

@@ -0,0 +1,27 @@
import { defineEventHandler, getRouterParam, readBody, createError } from 'h3'
import { requireAdmin } from '../../../utils/auth'
import { BugReport } from '../../../models/BugReport'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const status = body?.status
if (status !== 'open' && status !== 'resolved') {
throw createError({ statusCode: 400, statusMessage: 'status must be "open" or "resolved"' })
}
const doc = await BugReport.findByIdAndUpdate(
id,
{ $set: { status } },
{ new: true },
).lean() as any
if (!doc) {
throw createError({ statusCode: 404, statusMessage: 'Bug report not found' })
}
return { id: String(doc._id), status: doc.status }
})

View File

@@ -0,0 +1,78 @@
import { defineEventHandler, getQuery } from 'h3'
import { requireAdmin } from '../../../utils/auth'
import { BugReport } from '../../../models/BugReport'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
const query = getQuery(event)
const page = Math.max(parseInt(String(query.page ?? '1'), 10) || 1, 1)
const pageSize = 20
const skip = (page - 1) * pageSize
const status = String(query.status ?? 'open')
const filter: Record<string, any> = {}
if (status === 'open' || status === 'resolved') {
filter.status = status
}
const [docs, total] = await Promise.all([
BugReport.aggregate([
{ $match: filter },
{ $sort: { createdAt: -1 } },
{ $skip: skip },
{ $limit: pageSize },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'userDoc',
pipeline: [{ $project: { email: 1, name: 1, role: 1 } }],
},
},
{
$project: {
comment: 1,
contact: 1,
userId: 1,
userDoc: 1,
status: 1,
createdAt: 1,
'pmState.flowSlug': 1,
'pmState.scenarioId': 1,
'pmState.currentStateId': 1,
hasScreenshot: { $gt: [{ $strLenCP: { $ifNull: ['$screenshot', ''] } }, 0] },
},
},
]).exec(),
BugReport.countDocuments(filter),
])
const items = docs.map((doc: any) => ({
id: String(doc._id),
comment: doc.comment,
contact: doc.contact,
user: doc.userDoc?.[0]
? { id: String(doc.userDoc[0]._id), email: doc.userDoc[0].email, name: doc.userDoc[0].name }
: undefined,
pmState: {
flowSlug: doc.pmState?.flowSlug,
scenarioId: doc.pmState?.scenarioId,
currentStateId: doc.pmState?.currentStateId,
},
status: doc.status,
createdAt: doc.createdAt ? new Date(doc.createdAt).toISOString() : null,
hasScreenshot: Boolean(doc.hasScreenshot),
}))
return {
items,
pagination: {
total,
page,
pageSize,
pages: Math.ceil(total / pageSize) || 1,
},
}
})