mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 00:46:00 +08:00
fix(pm): make bug-report screenshots and state restore actually work
Screenshots: html2canvas 1.4.1 only parses rgb/hsl and throws on color-mix(), which the app uses app-wide; the throw was swallowed so no screenshot was ever captured. Swap to modern-screenshot (native SVG foreignObject — supports color-mix/oklch), capture before opening the dialog, surface failures instead of hiding them, and show a capture spinner on the Bug button. State restore: the old handler only patched local vars/flags and loaded the tree — it never entered the monitor screen or created a backend session, so nothing happened. New restoreBugReportState() reuses startMonitoring() to spin up a real session for the same flight/scenario from the snapshot, overlays saved variables/flags, and replays the captured comm log. The Python backend has no mid-session resume, so the session restarts at the flow start and the captured state id is surfaced in a banner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
125
app/pages/pm.vue
125
app/pages/pm.vue
@@ -471,10 +471,11 @@
|
||||
type="button"
|
||||
class="btn ghost"
|
||||
title="Fehler melden"
|
||||
:disabled="bugReportCapturing"
|
||||
@click="openBugReport"
|
||||
>
|
||||
<v-icon size="18">mdi-bug-outline</v-icon>
|
||||
<span class="btn-label">Bug</span>
|
||||
<v-icon size="18">{{ bugReportCapturing ? 'mdi-loading mdi-spin' : 'mdi-bug-outline' }}</v-icon>
|
||||
<span class="btn-label">{{ bugReportCapturing ? '…' : 'Bug' }}</span>
|
||||
</button>
|
||||
<NuxtLink class="btn ghost" to="/feedback" title="Share feedback or ideas">
|
||||
<v-icon size="18">mdi-message-draw</v-icon>
|
||||
@@ -2110,6 +2111,7 @@ const bugReportContact = ref('')
|
||||
const bugReportScreenshot = ref<string | null>(null)
|
||||
const bugReportArrows = ref<Array<{ fx: number; fy: number; tx: number; ty: number }>>([])
|
||||
const bugReportLoading = ref(false)
|
||||
const bugReportCapturing = ref(false)
|
||||
const bugReportError = ref('')
|
||||
const bugReportSuccess = ref(false)
|
||||
const bugReportCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -2206,11 +2208,28 @@ async function openBugReport() {
|
||||
bugReportScreenshot.value = null
|
||||
bugReportContact.value = [auth.user?.name, auth.user?.email].filter(Boolean).join(' — ')
|
||||
|
||||
// Capture the screenshot BEFORE opening the dialog, otherwise the open
|
||||
// dialog overlay would appear in the shot instead of the actual bug state.
|
||||
bugReportCapturing.value = true
|
||||
try {
|
||||
const { default: html2canvas } = await import('html2canvas')
|
||||
const c = await html2canvas(document.body, { scale: 0.55, useCORS: true, allowTaint: true, logging: false })
|
||||
bugReportScreenshot.value = c.toDataURL('image/jpeg', 0.75)
|
||||
} catch { /* Screenshot optional */ }
|
||||
// modern-screenshot renders via a native SVG <foreignObject>, so modern CSS
|
||||
// such as color-mix()/oklch() (used throughout the app) is supported.
|
||||
// html2canvas could not parse those and silently produced no screenshot.
|
||||
const { domToJpeg } = await import('modern-screenshot')
|
||||
bugReportScreenshot.value = await domToJpeg(document.body, {
|
||||
quality: 0.75,
|
||||
scale: 0.55,
|
||||
// Skip assets we cannot read (cross-origin tiles/avatars) instead of failing.
|
||||
filter: (node) => !(node instanceof Element && node.getAttribute?.('data-no-screenshot') === 'true'),
|
||||
})
|
||||
} catch (err) {
|
||||
// Screenshot is optional — keep the report flow usable, but surface why.
|
||||
console.warn('[PM] Bug report screenshot capture failed', err)
|
||||
bugReportScreenshot.value = null
|
||||
bugReportError.value = 'Screenshot konnte nicht erstellt werden – Bug-Report ohne Bild möglich.'
|
||||
} finally {
|
||||
bugReportCapturing.value = false
|
||||
}
|
||||
|
||||
showBugReportDialog.value = true
|
||||
}
|
||||
@@ -2263,6 +2282,83 @@ async function submitBugReport() {
|
||||
bugReportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a /pm session from a saved bug-report snapshot (admin link
|
||||
* `/pm?restoreBugReport=<id>`). The Python backend has no "resume mid-session"
|
||||
* endpoint, so we recreate a real, working session for the SAME flight and
|
||||
* scenario via startMonitoring(), then overlay the saved variables/flags and
|
||||
* the captured conversation so the admin can reproduce and try out the bug.
|
||||
*/
|
||||
async function restoreBugReportState(restoreId: string) {
|
||||
try {
|
||||
const report = await api.get<any>(`/api/admin/bug-reports/${restoreId}`)
|
||||
const state = report?.pmState
|
||||
if (!state) {
|
||||
error.value = 'Bug-Report enthält keinen gespeicherten State.'
|
||||
return
|
||||
}
|
||||
|
||||
// Locate the scenario the report was captured in.
|
||||
const scenario =
|
||||
SCENARIOS.find(s => s.id === state.scenarioId) ||
|
||||
SCENARIOS.find(s => s.startFlow === state.flowSlug)
|
||||
if (!scenario) {
|
||||
error.value = `Bug-Report-Restore: Szenario "${state.scenarioId || state.flowSlug || '?'}" nicht gefunden.`
|
||||
return
|
||||
}
|
||||
|
||||
// Reconstruct a flight plan from the snapshot so startMonitoring resolves the
|
||||
// correct airport/frequencies and creates a backend session for the same flight.
|
||||
const v = state.variables || {}
|
||||
const fc = state.flightContext || {}
|
||||
const dep = v.dep || fc.dep
|
||||
const dest = v.dest || fc.dest
|
||||
const flightPlan: Record<string, any> = {
|
||||
callsign: v.callsign || fc.callsign || 'UNKNOWN',
|
||||
aircraft: v.acf_type || fc.acf_type || 'A320',
|
||||
dep,
|
||||
departure: dep,
|
||||
arr: dest,
|
||||
arrival: dest,
|
||||
route: fc.route || v.route || '',
|
||||
assignedsquawk: v.squawk,
|
||||
}
|
||||
|
||||
// Spin up a real session (loads tree, fetches frequencies, creates backend session).
|
||||
await startMonitoring(flightPlan, scenario)
|
||||
// startMonitoring bails out on error without entering the monitor screen.
|
||||
if (currentScreen.value !== 'monitor') return
|
||||
|
||||
// Overlay the exact saved values over the freshly generated ones (stand, SID, …).
|
||||
if (state.variables && Object.keys(state.variables).length) patchVariables(state.variables)
|
||||
if (state.flags && Object.keys(state.flags).length) patchFlags(state.flags)
|
||||
|
||||
// Restore the captured conversation for context.
|
||||
clearCommunicationLog?.()
|
||||
if (Array.isArray(state.communicationLog)) {
|
||||
for (const e of state.communicationLog) {
|
||||
if (!e?.message) continue
|
||||
appendLogEntry(e.speaker || 'system', e.message, e.state || '', {
|
||||
frequency: e.frequency,
|
||||
flow: e.flow,
|
||||
radioCheck: e.radioCheck,
|
||||
offSchema: e.offSchema,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A fresh backend session always starts at the flow's start state, so we
|
||||
// can't fake the local cursor onto the captured mid-flow state without
|
||||
// desyncing transmits. Tell the admin where the bug was captured instead.
|
||||
error.value =
|
||||
`Bug-Report wiederhergestellt: ${scenario.name} · ${flightPlan.callsign} (${dep || '?'}→${dest || '?'}). ` +
|
||||
`Erfasster State: ${state.currentStateId || '?'} (Flow ${state.flowSlug || '?'}).`
|
||||
} catch (err) {
|
||||
console.warn('[PM] Bug report restore failed', err)
|
||||
error.value = 'Bug-Report konnte nicht wiederhergestellt werden.'
|
||||
}
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pre-recording (rolling mic buffer) so the first ~1s of PTT speech isn't clipped
|
||||
@@ -2428,22 +2524,7 @@ onMounted(async () => {
|
||||
// Restore state from bug report (admin link: /pm?restoreBugReport=<id>)
|
||||
const restoreId = route.query.restoreBugReport as string | undefined
|
||||
if (restoreId) {
|
||||
try {
|
||||
const report = await api.get<any>(`/api/admin/bug-reports/${restoreId}`)
|
||||
const state = report?.pmState
|
||||
if (state) {
|
||||
if (state.variables && Object.keys(state.variables).length) patchVariables(state.variables)
|
||||
if (state.flags && Object.keys(state.flags).length) patchFlags(state.flags)
|
||||
if (state.flowSlug) {
|
||||
try {
|
||||
await fetchRuntimeTree(state.flowSlug, config.public.radioBackendUrl as string)
|
||||
} catch {}
|
||||
}
|
||||
error.value = `[Bug-Report-Restore] State: ${state.currentStateId || '?'} · Flow: ${state.flowSlug || '?'}`
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[PM] Bug report restore failed', err)
|
||||
}
|
||||
await restoreBugReportState(restoreId)
|
||||
}
|
||||
} finally {
|
||||
restoringFromStorage = false
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"modern-screenshot": "^4.7.0",
|
||||
"nodemailer": "^8.0.1",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-aos": "^1.2.6",
|
||||
|
||||
53
yarn.lock
53
yarn.lock
@@ -4211,13 +4211,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-arraybuffer@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "base64-arraybuffer@npm:1.0.2"
|
||||
checksum: 10c0/3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
@@ -4834,15 +4827,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-line-break@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "css-line-break@npm:2.1.0"
|
||||
dependencies:
|
||||
utrie: "npm:^1.0.2"
|
||||
checksum: 10c0/b2222d99d5daf7861ecddc050244fdce296fad74b000dcff6bdfb1eb16dc2ef0b9ffe2c1c965e3239bd05ebe9eadb6d5438a91592fa8648d27a338e827cf9048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^5.1.0":
|
||||
version: 5.2.2
|
||||
resolution: "css-select@npm:5.2.2"
|
||||
@@ -6158,16 +6142,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html2canvas@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "html2canvas@npm:1.4.1"
|
||||
dependencies:
|
||||
css-line-break: "npm:^2.1.0"
|
||||
text-segmentation: "npm:^1.0.3"
|
||||
checksum: 10c0/6de86f75762b00948edf2ea559f16da0a1ec3facc4a8a7d3f35fcec59bb0c5970463478988ae3d9082152e0173690d46ebf4082e7ac803dd4817bae1d355c0db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-assert@npm:^1.3.0":
|
||||
version: 1.5.0
|
||||
resolution: "http-assert@npm:1.5.0"
|
||||
@@ -7339,6 +7313,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"modern-screenshot@npm:^4.7.0":
|
||||
version: 4.7.0
|
||||
resolution: "modern-screenshot@npm:4.7.0"
|
||||
checksum: 10c0/9c5fbe4f4c73d3dcf0ba7fb76c1671ad148fdedffc84700e0b8ca1c1640a6ab1930718ae1424b0a6517b98d8f18c898ea0e62ee0ca57a25bfce7b914df721a63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mongodb-connection-string-url@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "mongodb-connection-string-url@npm:3.0.2"
|
||||
@@ -8002,7 +7983,7 @@ __metadata:
|
||||
"@types/three": "npm:^0.183.0"
|
||||
dotenv: "npm:^17.3.1"
|
||||
fluent-ffmpeg: "npm:^2.1.3"
|
||||
html2canvas: "npm:^1.4.1"
|
||||
modern-screenshot: "npm:^4.7.0"
|
||||
nodemailer: "npm:^8.0.1"
|
||||
nuxt: "npm:^4.3.1"
|
||||
nuxt-aos: "npm:^1.2.6"
|
||||
@@ -10034,15 +10015,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"text-segmentation@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "text-segmentation@npm:1.0.3"
|
||||
dependencies:
|
||||
utrie: "npm:^1.0.2"
|
||||
checksum: 10c0/8b9ae8524e3a332371060d0ca62f10ad49a13e954719ea689a6c3a8b8c15c8a56365ede2bb91c322fb0d44b6533785f0da603e066b7554d052999967fb72d600
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thenify-all@npm:^1.0.0":
|
||||
version: 1.6.0
|
||||
resolution: "thenify-all@npm:1.6.0"
|
||||
@@ -10634,15 +10606,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"utrie@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "utrie@npm:1.0.2"
|
||||
dependencies:
|
||||
base64-arraybuffer: "npm:^1.0.2"
|
||||
checksum: 10c0/eaffe645bd81a39e4bc3abb23df5895e9961dbdd49748ef3b173529e8b06ce9dd1163e9705d5309a1c61ee41ffcb825e2043bc0fd1659845ffbdf4b1515dfdb4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"v-lazy-show@npm:^0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "v-lazy-show@npm:0.2.4"
|
||||
|
||||
Reference in New Issue
Block a user