diff --git a/app/components/live-atc/cockpit/BugReportDialog.vue b/app/components/BugReportDialog.vue
similarity index 94%
rename from app/components/live-atc/cockpit/BugReportDialog.vue
rename to app/components/BugReportDialog.vue
index e6dcabe..6310906 100644
--- a/app/components/live-atc/cockpit/BugReportDialog.vue
+++ b/app/components/BugReportDialog.vue
@@ -20,6 +20,7 @@ const {
bugReportLoading,
bugReportError,
bugReportSuccess,
+ bugReportCode,
bugReportCanvasRef,
bugReportImgRef,
setupAnnotationCanvas,
@@ -43,6 +44,9 @@ const {
Danke! Bug Report wurde gesendet.
+
+ Fehlercode {{ bugReportCode }}
+
@@ -103,7 +107,9 @@ const {
/** Scenario the report is captured in — stored alongside the state snapshot. */
- activeScenario: Ref<{ id?: string; startFlow?: string } | null | undefined>
+ activeScenario?: Ref<{ id?: string; startFlow?: string } | null | undefined>
}
-export function useBugReport(
- engine: ReturnType,
- deps: BugReportDeps,
-) {
+export function useBugReport(options: BugReportOptions) {
const api = useApi()
const auth = useAuthStore()
- const { currentState, variables: vars, flags, flightContext, communicationLog: log } = engine
- const { activeScenario } = deps
+ const { source, engine, activeScenario } = options
const showBugReportDialog = ref(false)
const bugReportComment = ref('')
@@ -28,6 +33,7 @@ export function useBugReport(
const bugReportCapturing = ref(false)
const bugReportError = ref('')
const bugReportSuccess = ref(false)
+ const bugReportCode = ref('')
const bugReportCanvasRef = ref(null)
const bugReportImgRef = ref(null)
let _arrowDrawing = false
@@ -115,6 +121,7 @@ export function useBugReport(
async function openBugReport() {
bugReportError.value = ''
bugReportSuccess.value = false
+ bugReportCode.value = ''
bugReportComment.value = ''
bugReportArrows.value = []
bugReportScreenshot.value = null
@@ -147,7 +154,7 @@ export function useBugReport(
}
async function submitBugReport() {
- if (!bugReportComment.value.trim()) { bugReportError.value = 'Bitte einen Kommentar eingeben.'; return }
+ if (!bugReportComment.value.trim()) { bugReportError.value = 'Bitte eine Fehlerbeschreibung eingeben.'; return }
bugReportLoading.value = true
bugReportError.value = ''
@@ -169,25 +176,29 @@ export function useBugReport(
finalScreenshot = out.toDataURL('image/jpeg', 0.8)
}
- const pmState = {
- flowSlug: activeScenario.value?.startFlow || '',
- scenarioId: activeScenario.value?.id || '',
- currentStateId: (currentState.value as any)?.id || '',
- variables: (vars as any)?.value || {},
- flags: (flags as any)?.value || {},
- flightContext: (flightContext as any)?.value || {},
- communicationLog: ((log as any)?.value || [] as any[]).slice(-20),
- }
+ const pmState = engine
+ ? {
+ flowSlug: activeScenario?.value?.startFlow || '',
+ scenarioId: activeScenario?.value?.id || '',
+ currentStateId: (engine.currentState.value as any)?.id || '',
+ variables: (engine.variables as any)?.value || {},
+ flags: (engine.flags as any)?.value || {},
+ flightContext: (engine.flightContext as any)?.value || {},
+ communicationLog: ((engine.communicationLog as any)?.value || [] as any[]).slice(-20),
+ }
+ : undefined
- await api.post('/api/bug-reports', {
+ const res = await api.post<{ code?: string }>('/api/bug-reports', {
+ source,
comment: bugReportComment.value.trim(),
contact: bugReportContact.value.trim(),
screenshot: finalScreenshot,
pmState,
})
+ bugReportCode.value = res?.code || ''
bugReportSuccess.value = true
- setTimeout(() => { showBugReportDialog.value = false; bugReportSuccess.value = false }, 2500)
+ setTimeout(() => { showBugReportDialog.value = false; bugReportSuccess.value = false }, 5000)
} catch (err: any) {
bugReportError.value = err?.data?.statusMessage || err?.message || 'Fehler beim Senden.'
} finally {
@@ -205,6 +216,7 @@ export function useBugReport(
bugReportCapturing,
bugReportError,
bugReportSuccess,
+ bugReportCode,
bugReportCanvasRef,
bugReportImgRef,
setupAnnotationCanvas,
diff --git a/app/pages/classroom.vue b/app/pages/classroom.vue
index 4523eff..4c2370b 100644
--- a/app/pages/classroom.vue
+++ b/app/pages/classroom.vue
@@ -129,10 +129,15 @@
-
- mdi-message-draw
- Feedback
-
+
@@ -1420,6 +1427,9 @@
+
+
+
mdi-trophy
@@ -1457,6 +1467,8 @@ import {
classroomVoiceFor,
} from '~~/shared/utils/voicePool'
import {DEFAULT_AIRLINE_TELEPHONY, normalizeRadioPhrase, normalizeMetarPhrase} from '~~/shared/utils/radioSpeech'
+import {useBugReport} from '~/composables/useBugReport'
+import BugReportDialog from '~/components/BugReportDialog.vue'
definePageMeta({middleware: ['require-auth', 'require-classroom-intro']})
@@ -2798,6 +2810,11 @@ const showOnlineTtsSuggestion = ref(false)
const api = useApi()
const isClient = typeof window !== 'undefined'
const auth = useAuthStore()
+
+// Bug reports work exactly as in Live ATC (screenshot + arrow annotation), just
+// without a communications engine to snapshot — the source tells the mail apart.
+const bugReport = useBugReport({source: 'classroom'})
+const {bugReportCapturing, openBugReport} = bugReport
const browserTtsAvailable = computed(() => isClient && 'speechSynthesis' in window)
// STT (Speech-to-Text) for the readback — pilot speaks the readback into a mic,
@@ -6192,6 +6209,17 @@ onBeforeUnmount(() => {
gap: 6px
}
+.link-button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0
+}
+
+.link-button:disabled {
+ opacity: .6;
+ cursor: default
+}
+
.stats {
display: flex;
gap: 8px
diff --git a/app/pages/live-atc.vue b/app/pages/live-atc.vue
index 8f9e49a..48d26dc 100644
--- a/app/pages/live-atc.vue
+++ b/app/pages/live-atc.vue
@@ -185,7 +185,7 @@ import { useBugReport } from '~/composables/useBugReport'
import { useSimBridgeSync } from '~/composables/useSimBridgeSync'
import FlightInfoSheet from '~/components/live-atc/cockpit/FlightInfoSheet.vue'
import SettingsSheet from '~/components/live-atc/cockpit/SettingsSheet.vue'
-import BugReportDialog from '~/components/live-atc/cockpit/BugReportDialog.vue'
+import BugReportDialog from '~/components/BugReportDialog.vue'
import TransmissionIssueDialog from '~/components/live-atc/cockpit/TransmissionIssueDialog.vue'
import HelpDialog from '~/components/live-atc/cockpit/HelpDialog.vue'
import DebugPanel from '~/components/live-atc/cockpit/DebugPanel.vue'
@@ -320,7 +320,7 @@ const aiTrafficEnabled = ref(false)
// ── Bug Report ───────────────────────────────────────────────────────────────
// Owned here rather than by the dialog: the HUD button starts the screenshot
// capture before the dialog ever renders. The dialog gets the whole handle.
-const bugReport = useBugReport(engine, { activeScenario })
+const bugReport = useBugReport({ source: 'live-atc', engine, activeScenario })
const { bugReportCapturing, openBugReport } = bugReport
// Layout / view state. The tab, HUD menu and mode-switch state live inside
diff --git a/server/api/bug-reports/index.post.ts b/server/api/bug-reports/index.post.ts
index 4ac8bd7..63313a4 100644
--- a/server/api/bug-reports/index.post.ts
+++ b/server/api/bug-reports/index.post.ts
@@ -1,22 +1,35 @@
import { defineEventHandler, readBody, createError } from 'h3'
+import { randomUUID } from 'node:crypto'
import { requireUserSession } from '../../utils/auth'
-import { BugReport } from '../../models/BugReport'
+import { BugReport, type BugReportSource } from '../../models/BugReport'
import { sendMail } from '../../utils/notifications'
+const SOURCE_LABELS: Record = {
+ 'live-atc': 'Live ATC',
+ classroom: 'Classroom',
+}
+
export default defineEventHandler(async (event) => {
const user = await requireUserSession(event)
const body = await readBody(event)
const comment = String(body?.comment ?? '').trim()
if (!comment) {
- throw createError({ statusCode: 400, statusMessage: 'Kommentar ist erforderlich' })
+ throw createError({ statusCode: 400, statusMessage: 'Fehlerbeschreibung ist erforderlich' })
}
const contact = String(
body?.contact || [user.name, user.email].filter(Boolean).join(' — ')
).slice(0, 200)
+ const source: BugReportSource = body?.source === 'classroom' ? 'classroom' : 'live-atc'
+ // The code is what ties a report to the commit that fixes it — it goes into
+ // the mail so it can be pasted straight into the commit message.
+ const code = randomUUID()
+
const report = await BugReport.create({
+ code,
+ source,
comment: comment.slice(0, 4000),
contact,
userId: user._id,
@@ -25,20 +38,32 @@ export default defineEventHandler(async (event) => {
})
const adminUrl = `${process.env.APP_URL || 'https://app.opensquawk.de'}/admin`
+ const sourceLabel = SOURCE_LABELS[source]
const stateInfo = body?.pmState?.currentStateId
? `State: ${body.pmState.currentStateId} (Flow: ${body.pmState.flowSlug || '—'})`
: ''
await sendMail({
to: 'emanuel@faktorxmensch.com',
- subject: `[OpenSquawk Bug] ${contact}`,
+ subject: `[OpenSquawk Bug · ${sourceLabel}] ${contact}`,
html: `Neuer Bug Report
+Bereich: ${sourceLabel}
Von: ${contact}
-Kommentar:
${comment.replace(/\n/g, '
')}
+Fehlerbeschreibung/Featurewunsch:
${comment.replace(/\n/g, '
')}
+Nenne den Fehlercode ${code} beim Commit.
${stateInfo ? `${stateInfo}
` : ''}
Im Admin-Panel ansehen →
`,
- text: `Bug Report von ${contact}\n\n${comment}\n${stateInfo}\n\nAdmin: ${adminUrl}`,
+ text: `Bug Report von ${contact}
+Bereich: ${sourceLabel}
+
+Fehlerbeschreibung/Featurewunsch:
+${comment}
+
+Nenne den Fehlercode ${code} beim Commit.
+${stateInfo}
+
+Admin: ${adminUrl}`,
}).catch(() => {})
- return { success: true, id: String(report._id) }
+ return { success: true, id: String(report._id), code }
})
diff --git a/server/models/BugReport.ts b/server/models/BugReport.ts
index 63a73c7..61ecbcb 100644
--- a/server/models/BugReport.ts
+++ b/server/models/BugReport.ts
@@ -2,6 +2,10 @@ import mongoose from 'mongoose'
export type BugReportStatus = 'open' | 'resolved'
+/** Where the report was filed. Shown in the notification mail so we know which
+ * part of the app to reproduce it in. */
+export type BugReportSource = 'live-atc' | 'classroom'
+
export interface PmStateSnapshot {
flowSlug?: string
scenarioId?: string
@@ -13,6 +17,9 @@ export interface PmStateSnapshot {
}
export interface BugReportDocument extends mongoose.Document {
+ /** Stable UUID quoted in commit messages so a fix can be traced back here. */
+ code: string
+ source: BugReportSource
comment: string
contact: string
userId?: mongoose.Types.ObjectId
@@ -23,6 +30,10 @@ export interface BugReportDocument extends mongoose.Document {
}
const bugReportSchema = new mongoose.Schema({
+ // Sparse: reports created before the code existed simply have none, and a
+ // plain unique index would reject more than one of them.
+ code: { type: String, required: true, unique: true, sparse: true },
+ source: { type: String, enum: ['live-atc', 'classroom'], default: 'live-atc', index: true },
comment: { type: String, required: true, trim: true, maxlength: 4000 },
contact: { type: String, required: true, trim: true, maxlength: 200 },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },