mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-04 16:22:48 +08:00
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:
Binary file not shown.
296
app/pages/pm.vue
296
app/pages/pm.vue
@@ -467,6 +467,15 @@
|
||||
</div>
|
||||
|
||||
<div class="hud-right">
|
||||
<button
|
||||
type="button"
|
||||
class="btn ghost"
|
||||
title="Fehler melden"
|
||||
@click="openBugReport"
|
||||
>
|
||||
<v-icon size="18">mdi-bug-outline</v-icon>
|
||||
<span class="btn-label">Bug</span>
|
||||
</button>
|
||||
<NuxtLink class="btn ghost" to="/feedback" title="Share feedback or ideas">
|
||||
<v-icon size="18">mdi-message-draw</v-icon>
|
||||
<span class="btn-label">Feedback</span>
|
||||
@@ -1198,6 +1207,107 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Bug Report Dialog -->
|
||||
<v-dialog v-model="showBugReportDialog" max-width="680" scrollable>
|
||||
<v-card class="bg-[#0b101d] border border-white/10 text-white">
|
||||
<v-card-title class="flex items-center gap-2 text-base font-semibold pt-4 px-5">
|
||||
<v-icon icon="mdi-bug-outline" color="#f87171" size="20" />
|
||||
Fehler melden
|
||||
</v-card-title>
|
||||
<v-card-text class="space-y-4 px-5 pb-2">
|
||||
<div v-if="bugReportSuccess" class="rounded-xl bg-emerald-500/10 border border-emerald-400/30 p-4 text-center">
|
||||
<v-icon icon="mdi-check-circle-outline" color="emerald" size="32" class="mb-2" />
|
||||
<p class="text-emerald-300 font-semibold">Danke! Bug Report wurde gesendet.</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="bugReportScreenshot" class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Screenshot – Pfeile einzeichnen</p>
|
||||
<v-btn
|
||||
v-if="bugReportArrows.length > 0"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="red"
|
||||
prepend-icon="mdi-undo"
|
||||
@click="undoLastArrow"
|
||||
>
|
||||
Rückgängig
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="relative rounded-xl overflow-hidden border border-white/10" style="line-height:0">
|
||||
<img
|
||||
ref="bugReportImgRef"
|
||||
:src="bugReportScreenshot"
|
||||
class="w-full block"
|
||||
alt="Screenshot"
|
||||
@load="setupAnnotationCanvas"
|
||||
/>
|
||||
<canvas
|
||||
ref="bugReportCanvasRef"
|
||||
class="absolute inset-0 w-full h-full cursor-crosshair select-none"
|
||||
style="touch-action:none"
|
||||
@mousedown="onCanvasMouseDown"
|
||||
@mousemove="onCanvasMouseMove"
|
||||
@mouseup="onCanvasMouseUp"
|
||||
@mouseleave="onCanvasMouseLeave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rounded-xl border border-white/10 bg-white/5 p-4 text-center text-sm text-white/50">
|
||||
Kein Screenshot verfügbar
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-model="bugReportComment"
|
||||
label="Was ist kaputt? Was sollte stattdessen passieren?"
|
||||
variant="outlined"
|
||||
color="red"
|
||||
rows="3"
|
||||
auto-grow
|
||||
maxlength="2000"
|
||||
counter="2000"
|
||||
hide-details="auto"
|
||||
:disabled="bugReportLoading"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="bugReportContact"
|
||||
label="Dein Name / Kontakt"
|
||||
variant="outlined"
|
||||
color="red"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
:disabled="bugReportLoading"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="bugReportError"
|
||||
type="error"
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
class="bg-red-500/10 text-red-200"
|
||||
>
|
||||
{{ bugReportError }}
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="!bugReportSuccess" class="justify-end gap-2 px-5 pb-4">
|
||||
<v-btn variant="text" color="grey" :disabled="bugReportLoading" @click="showBugReportDialog = false">
|
||||
Abbrechen
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
variant="flat"
|
||||
:loading="bugReportLoading"
|
||||
:disabled="!bugReportComment.trim()"
|
||||
@click="submitBugReport"
|
||||
>
|
||||
Bug melden
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Transmission issue dialog -->
|
||||
<v-dialog v-model="showTransmissionIssueDialog" max-width="420">
|
||||
<v-card class="bg-[#0b101d] border border-white/10 text-white">
|
||||
@@ -1242,7 +1352,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import useCommunicationsEngine from "../../shared/utils/communicationsEngine";
|
||||
import { normalizeRadioPhrase, normalizeAtisForSpeech, DEFAULT_AIRLINE_TELEPHONY } from '../../shared/utils/radioSpeech';
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
@@ -1292,6 +1402,7 @@ const engine = useCommunicationsEngine()
|
||||
const auth = useAuthStore()
|
||||
const api = useApi()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const radioBackend = useRadioBackend()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
@@ -1992,6 +2103,168 @@ const radioEffectsEnabled = ref(true)
|
||||
const readbackEnabled = ref(false)
|
||||
const debugMode = ref(true)
|
||||
|
||||
// ── Bug Report ───────────────────────────────────────────────────────────────
|
||||
const showBugReportDialog = ref(false)
|
||||
const bugReportComment = ref('')
|
||||
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 bugReportError = ref('')
|
||||
const bugReportSuccess = ref(false)
|
||||
const bugReportCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const bugReportImgRef = ref<HTMLImageElement | null>(null)
|
||||
let _arrowDrawing = false
|
||||
let _arrowStart = { x: 0, y: 0 }
|
||||
|
||||
function setupAnnotationCanvas() {
|
||||
const canvas = bugReportCanvasRef.value
|
||||
const img = bugReportImgRef.value
|
||||
if (!canvas || !img) return
|
||||
canvas.width = img.clientWidth
|
||||
canvas.height = img.clientHeight
|
||||
}
|
||||
|
||||
function _drawArrow(ctx: CanvasRenderingContext2D, fx: number, fy: number, tx: number, ty: number) {
|
||||
const headLen = 14
|
||||
const angle = Math.atan2(ty - fy, tx - fx)
|
||||
ctx.strokeStyle = '#ef4444'
|
||||
ctx.fillStyle = '#ef4444'
|
||||
ctx.lineWidth = 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(fx, fy)
|
||||
ctx.lineTo(tx, ty)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(tx, ty)
|
||||
ctx.lineTo(tx - headLen * Math.cos(angle - Math.PI / 6), ty - headLen * Math.sin(angle - Math.PI / 6))
|
||||
ctx.lineTo(tx - headLen * Math.cos(angle + Math.PI / 6), ty - headLen * Math.sin(angle + Math.PI / 6))
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
function _redrawAnnotations(preview?: { fx: number; fy: number; tx: number; ty: number }) {
|
||||
const canvas = bugReportCanvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
for (const a of bugReportArrows.value) _drawArrow(ctx, a.fx, a.fy, a.tx, a.ty)
|
||||
if (preview) {
|
||||
ctx.globalAlpha = 0.55
|
||||
_drawArrow(ctx, preview.fx, preview.fy, preview.tx, preview.ty)
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
function _canvasCoords(e: MouseEvent) {
|
||||
const canvas = bugReportCanvasRef.value!
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||
}
|
||||
|
||||
function onCanvasMouseDown(e: MouseEvent) {
|
||||
_arrowStart = _canvasCoords(e)
|
||||
_arrowDrawing = true
|
||||
}
|
||||
|
||||
function onCanvasMouseMove(e: MouseEvent) {
|
||||
if (!_arrowDrawing) return
|
||||
const { x, y } = _canvasCoords(e)
|
||||
_redrawAnnotations({ fx: _arrowStart.x, fy: _arrowStart.y, tx: x, ty: y })
|
||||
}
|
||||
|
||||
function onCanvasMouseUp(e: MouseEvent) {
|
||||
if (!_arrowDrawing) return
|
||||
_arrowDrawing = false
|
||||
const { x, y } = _canvasCoords(e)
|
||||
const dx = x - _arrowStart.x
|
||||
const dy = y - _arrowStart.y
|
||||
if (Math.sqrt(dx * dx + dy * dy) < 8) { _redrawAnnotations(); return }
|
||||
bugReportArrows.value = [...bugReportArrows.value, { fx: _arrowStart.x, fy: _arrowStart.y, tx: x, ty: y }]
|
||||
_redrawAnnotations()
|
||||
}
|
||||
|
||||
function onCanvasMouseLeave() {
|
||||
if (!_arrowDrawing) return
|
||||
_arrowDrawing = false
|
||||
_redrawAnnotations()
|
||||
}
|
||||
|
||||
function undoLastArrow() {
|
||||
bugReportArrows.value = bugReportArrows.value.slice(0, -1)
|
||||
_redrawAnnotations()
|
||||
}
|
||||
|
||||
async function openBugReport() {
|
||||
bugReportError.value = ''
|
||||
bugReportSuccess.value = false
|
||||
bugReportComment.value = ''
|
||||
bugReportArrows.value = []
|
||||
bugReportScreenshot.value = null
|
||||
bugReportContact.value = [auth.user?.name, auth.user?.email].filter(Boolean).join(' — ')
|
||||
|
||||
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 */ }
|
||||
|
||||
showBugReportDialog.value = true
|
||||
}
|
||||
|
||||
async function submitBugReport() {
|
||||
if (!bugReportComment.value.trim()) { bugReportError.value = 'Bitte einen Kommentar eingeben.'; return }
|
||||
bugReportLoading.value = true
|
||||
bugReportError.value = ''
|
||||
|
||||
try {
|
||||
let finalScreenshot: string | undefined
|
||||
if (bugReportScreenshot.value) {
|
||||
const img = bugReportImgRef.value
|
||||
const src = new Image()
|
||||
await new Promise<void>((res) => { src.onload = () => res(); src.src = bugReportScreenshot.value! })
|
||||
const out = document.createElement('canvas')
|
||||
out.width = src.naturalWidth; out.height = src.naturalHeight
|
||||
const ctx = out.getContext('2d')!
|
||||
ctx.drawImage(src, 0, 0)
|
||||
if (bugReportArrows.value.length > 0 && img) {
|
||||
const sx = src.naturalWidth / img.clientWidth
|
||||
const sy = src.naturalHeight / img.clientHeight
|
||||
for (const a of bugReportArrows.value) _drawArrow(ctx, a.fx * sx, a.fy * sy, a.tx * sx, a.ty * sy)
|
||||
}
|
||||
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),
|
||||
}
|
||||
|
||||
await api.post('/api/bug-reports', {
|
||||
comment: bugReportComment.value.trim(),
|
||||
contact: bugReportContact.value.trim(),
|
||||
screenshot: finalScreenshot,
|
||||
pmState,
|
||||
})
|
||||
|
||||
bugReportSuccess.value = true
|
||||
setTimeout(() => { showBugReportDialog.value = false; bugReportSuccess.value = false }, 2500)
|
||||
} catch (err: any) {
|
||||
bugReportError.value = err?.data?.statusMessage || err?.message || 'Fehler beim Senden.'
|
||||
} finally {
|
||||
bugReportLoading.value = false
|
||||
}
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pre-recording (rolling mic buffer) so the first ~1s of PTT speech isn't clipped
|
||||
const prerecEnabled = ref(true)
|
||||
const prerecSeconds = ref(1.0)
|
||||
@@ -2151,6 +2424,27 @@ 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)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
restoringFromStorage = false
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"nodemailer": "^8.0.1",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-aos": "^1.2.6",
|
||||
|
||||
44
server/api/bug-reports/index.post.ts
Normal file
44
server/api/bug-reports/index.post.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineEventHandler, readBody, createError } from 'h3'
|
||||
import { requireUserSession } from '../../utils/auth'
|
||||
import { BugReport } from '../../models/BugReport'
|
||||
import { sendMail } from '../../utils/notifications'
|
||||
|
||||
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' })
|
||||
}
|
||||
|
||||
const contact = String(
|
||||
body?.contact || [user.name, user.email].filter(Boolean).join(' — ')
|
||||
).slice(0, 200)
|
||||
|
||||
const report = await BugReport.create({
|
||||
comment: comment.slice(0, 4000),
|
||||
contact,
|
||||
userId: user._id,
|
||||
screenshot: body?.screenshot || undefined,
|
||||
pmState: body?.pmState || undefined,
|
||||
})
|
||||
|
||||
const adminUrl = `${process.env.APP_URL || 'https://app.opensquawk.de'}/admin`
|
||||
const stateInfo = body?.pmState?.currentStateId
|
||||
? `State: ${body.pmState.currentStateId} (Flow: ${body.pmState.flowSlug || '—'})`
|
||||
: ''
|
||||
|
||||
await sendMail({
|
||||
to: 'emanuel@faktorxmensch.com',
|
||||
subject: `[OpenSquawk Bug] ${contact}`,
|
||||
html: `<h2>Neuer Bug Report</h2>
|
||||
<p><strong>Von:</strong> ${contact}</p>
|
||||
<p><strong>Kommentar:</strong><br>${comment.replace(/\n/g, '<br>')}</p>
|
||||
${stateInfo ? `<p><strong>${stateInfo}</strong></p>` : ''}
|
||||
<p><a href="${adminUrl}">Im Admin-Panel ansehen →</a></p>`,
|
||||
text: `Bug Report von ${contact}\n\n${comment}\n${stateInfo}\n\nAdmin: ${adminUrl}`,
|
||||
}).catch(() => {})
|
||||
|
||||
return { success: true, id: String(report._id) }
|
||||
})
|
||||
45
server/models/BugReport.ts
Normal file
45
server/models/BugReport.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export type BugReportStatus = 'open' | 'resolved'
|
||||
|
||||
export interface PmStateSnapshot {
|
||||
flowSlug?: string
|
||||
scenarioId?: string
|
||||
currentStateId?: string
|
||||
variables?: Record<string, any>
|
||||
flags?: Record<string, boolean>
|
||||
flightContext?: Record<string, any>
|
||||
communicationLog?: any[]
|
||||
}
|
||||
|
||||
export interface BugReportDocument extends mongoose.Document {
|
||||
comment: string
|
||||
contact: string
|
||||
userId?: mongoose.Types.ObjectId
|
||||
screenshot?: string
|
||||
pmState?: PmStateSnapshot
|
||||
status: BugReportStatus
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
const bugReportSchema = new mongoose.Schema<BugReportDocument>({
|
||||
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 },
|
||||
screenshot: { type: String },
|
||||
pmState: {
|
||||
flowSlug: { type: String },
|
||||
scenarioId: { type: String },
|
||||
currentStateId: { type: String },
|
||||
variables: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
flags: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
flightContext: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
communicationLog: { type: [mongoose.Schema.Types.Mixed], default: [] },
|
||||
},
|
||||
status: { type: String, enum: ['open', 'resolved'], default: 'open', index: true },
|
||||
createdAt: { type: Date, default: () => new Date(), index: true },
|
||||
})
|
||||
|
||||
export const BugReport =
|
||||
(mongoose.models.BugReport as mongoose.Model<BugReportDocument> | undefined) ||
|
||||
mongoose.model<BugReportDocument>('BugReport', bugReportSchema)
|
||||
45
yarn.lock
45
yarn.lock
@@ -4211,6 +4211,13 @@ __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"
|
||||
@@ -4827,6 +4834,15 @@ __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"
|
||||
@@ -6142,6 +6158,16 @@ __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"
|
||||
@@ -7976,6 +8002,7 @@ __metadata:
|
||||
"@types/three": "npm:^0.183.0"
|
||||
dotenv: "npm:^17.3.1"
|
||||
fluent-ffmpeg: "npm:^2.1.3"
|
||||
html2canvas: "npm:^1.4.1"
|
||||
nodemailer: "npm:^8.0.1"
|
||||
nuxt: "npm:^4.3.1"
|
||||
nuxt-aos: "npm:^1.2.6"
|
||||
@@ -10007,6 +10034,15 @@ __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"
|
||||
@@ -10598,6 +10634,15 @@ __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