Files
PilotVault/Web App/web/src/components/Logbook.vue
T
tajniak81andClaude Opus 4.8 e9b27530ec Add drone-pilot logbook system (BEK 1649 §5)
Model Denmark's Dronebekendtgørelsen § 5 (on top of EU 2019/947) across all
three tiers: schema, API Server, and Web App.

Schema (migration 1720300700_add_logbook.js): two collections — `drones`
(classification inputs: mtom, is_toy, autologs, c_class, operator no.) and
`flights` (§5 minimum content + category/purpose/logging-path + operational
maturity + a retention_until computed as operation_date + 5y). Locked API
rules; access flows through the service account like users/orgs.

API Server (logbook.go, logbook_export.go): /api/drones and /api/flights CRUD
with per-role scoping in Go (user→own, admin→org, superadmin→all), plus
GET /api/logbook/export (CSV — the "readable electronic format" for
Trafikstyrelsen / pending police disclosure). Compliance is computed
server-side per flight: exemption (toy / club-area / <250 g hobby), effective
logging path, and red flags (autologs-without-FDR, specific-category-without-
authorisation, missing §5 fields, past retention). Manual-path saves missing a
§5 field are blocked (422).

Web App: BFF proxies (export preserves the CSV Content-Type/Disposition),
api.js client fns, and a Logbook.vue view (Flights/Drones tabs, inline forms,
compliance badges + expandable detail, Export CSV) wired into Dashboard.vue,
replacing the placeholder. Includes the rebuilt embedded dist bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:11:39 +02:00

552 lines
25 KiB
Vue

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import Icon from './Icon.vue'
import {
getDrones, createDrone, updateDrone, deleteDrone,
getFlights, createFlight, updateFlight, deleteFlight, exportLogbookUrl,
} from '../api.js'
const props = defineProps({
email: { type: String, default: '' },
role: { type: String, default: 'user' },
organization: { type: String, default: '' },
organizationName: { type: String, default: '' },
})
const badgeClass = {
success: 'bg-success-soft text-success-fg',
warning: 'bg-amber-soft text-amber-fg',
danger: 'bg-danger-soft text-danger-fg',
accent: 'bg-accent-soft text-accent-soft-fg',
neutral: 'bg-surface-2 text-ink-secondary',
}
const tab = ref('flights') // 'flights' | 'drones'
const drones = ref([])
const flights = ref([])
const loading = ref(false)
const loadErr = ref('')
const droneById = computed(() => Object.fromEntries(drones.value.map((d) => [d.id, d])))
async function loadAll() {
loading.value = true
loadErr.value = ''
const [dr, fl] = await Promise.all([getDrones(), getFlights()])
if (!dr.ok || !fl.ok) {
loadErr.value =
dr.status === 503 || fl.status === 503
? 'Logbook storage is not configured on the API Server (service account missing).'
: 'Could not load the logbook.'
}
drones.value = dr.drones
flights.value = fl.flights
loading.value = false
}
onMounted(loadAll)
/* ---------------- compliance presentation ---------------- */
function flightBadge(f) {
const c = f.compliance || {}
if (c.exempt) return { tone: 'neutral', label: 'Exempt' }
if ((c.redFlags || []).length) return { tone: 'danger', label: `${c.redFlags.length} issue${c.redFlags.length > 1 ? 's' : ''}` }
return { tone: 'success', label: 'Compliant' }
}
const openRow = ref('') // flight id whose compliance detail is expanded
function toggleRow(id) {
openRow.value = openRow.value === id ? '' : id
}
/* ---------------- flight form ---------------- */
const CATEGORIES = [
{ value: 'open', label: 'Open' },
{ value: 'specific', label: 'Specific' },
{ value: 'certified', label: 'Certified' },
]
const PURPOSES = [
{ value: 'commercial', label: 'Commercial' },
{ value: 'research', label: 'Research' },
{ value: 'public', label: 'Public-benefit' },
{ value: 'hobby', label: 'Private hobby' },
{ value: 'club_area', label: 'Model-club area' },
]
const PATHS = [
{ value: '', label: 'Auto (from drone)' },
{ value: 'manual', label: 'Manual' },
{ value: 'automatic', label: 'Automatic (FDR)' },
]
function blankFlight() {
return {
operationDate: new Date().toISOString().slice(0, 10),
startTime: '', endTime: '', drone: drones.value[0]?.id || '',
areaRoute: '', maxAltitudeAgl: '', pilotName: props.email, certificateRef: '',
category: 'open', purpose: 'commercial', loggingPath: '',
rawFdrLogUrl: '', authorisationRef: '',
weather: '', airspaceRef: '', observer: '', incidents: '', notes: '',
}
}
const showFlightForm = ref(false)
const editingFlightId = ref('')
const flightForm = reactive(blankFlight())
const flightMsg = ref('')
const savingFlight = ref(false)
const showDetails = ref(false)
function newFlight() {
Object.assign(flightForm, blankFlight())
editingFlightId.value = ''
flightMsg.value = ''
showDetails.value = false
showFlightForm.value = true
}
function editFlight(f) {
Object.assign(flightForm, {
operationDate: (f.operationDate || '').slice(0, 10),
startTime: f.startTime || '', endTime: f.endTime || '', drone: f.drone || '',
areaRoute: f.areaRoute || '', maxAltitudeAgl: f.maxAltitudeAgl || '',
pilotName: f.pilotName || '', certificateRef: f.certificateRef || '',
category: f.category || 'open', purpose: f.purpose || 'commercial',
loggingPath: f.loggingPath || '', rawFdrLogUrl: f.rawFdrLogUrl || '',
authorisationRef: f.authorisationRef || '', weather: f.weather || '',
airspaceRef: f.airspaceRef || '', observer: f.observer || '',
incidents: f.incidents || '', notes: f.notes || '',
})
editingFlightId.value = f.id
flightMsg.value = ''
showDetails.value = !!(f.weather || f.airspaceRef || f.observer || f.incidents || f.notes)
showFlightForm.value = true
}
function cancelFlight() {
showFlightForm.value = false
editingFlightId.value = ''
}
async function saveFlight() {
flightMsg.value = ''
if (!flightForm.drone) {
flightMsg.value = 'Select a drone first (add one on the Drones tab).'
return
}
savingFlight.value = true
const payload = { ...flightForm, maxAltitudeAgl: Number(flightForm.maxAltitudeAgl) || 0 }
const res = editingFlightId.value
? await updateFlight(editingFlightId.value, payload)
: await createFlight(payload)
savingFlight.value = false
if (!res.ok) {
flightMsg.value = res.body?.error || 'Could not save the flight.'
return
}
showFlightForm.value = false
await loadAll()
}
const confirmFlightId = ref('')
async function removeFlight(f) {
const res = await deleteFlight(f.id)
confirmFlightId.value = ''
if (res.ok) await loadAll()
}
/* ---------------- drone form ---------------- */
const C_CLASSES = ['', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6']
function blankDrone() {
return {
name: '', model: '', serial: '', operatorNumber: '',
mtomGrams: '', isToy: false, autologsFlights: false, cClass: '',
}
}
const showDroneForm = ref(false)
const editingDroneId = ref('')
const droneForm = reactive(blankDrone())
const droneMsg = ref('')
const savingDrone = ref(false)
function newDrone() {
Object.assign(droneForm, blankDrone())
editingDroneId.value = ''
droneMsg.value = ''
showDroneForm.value = true
}
function editDrone(d) {
Object.assign(droneForm, {
name: d.name || '', model: d.model || '', serial: d.serial || '',
operatorNumber: d.operatorNumber || '', mtomGrams: d.mtomGrams || '',
isToy: !!d.isToy, autologsFlights: !!d.autologsFlights, cClass: d.cClass || '',
})
editingDroneId.value = d.id
droneMsg.value = ''
showDroneForm.value = true
}
function cancelDrone() {
showDroneForm.value = false
editingDroneId.value = ''
}
async function saveDrone() {
droneMsg.value = ''
if (!droneForm.name.trim()) {
droneMsg.value = 'Give the drone a name.'
return
}
savingDrone.value = true
const payload = { ...droneForm, mtomGrams: Number(droneForm.mtomGrams) || 0 }
const res = editingDroneId.value
? await updateDrone(editingDroneId.value, payload)
: await createDrone(payload)
savingDrone.value = false
if (!res.ok) {
droneMsg.value = res.body?.error || 'Could not save the drone.'
return
}
showDroneForm.value = false
await loadAll()
}
const confirmDroneId = ref('')
async function removeDrone(d) {
const res = await deleteDrone(d.id)
confirmDroneId.value = ''
if (res.ok) await loadAll()
else droneMsg.value = res.body?.error || 'Could not delete the drone.'
}
/* ---------------- headline stats ---------------- */
const stats = computed(() => {
const total = flights.value.length
const flagged = flights.value.filter((f) => (f.compliance?.redFlags || []).length).length
const required = flights.value.filter((f) => f.compliance?.required).length
return { total, flagged, required, fleet: drones.value.length }
})
</script>
<template>
<div class="mx-auto flex max-w-[1240px] flex-col gap-5 p-7">
<!-- header + actions -->
<div class="flex flex-wrap items-center gap-3">
<div class="inline-flex rounded-lg border border-line bg-surface-1 p-0.5">
<button
v-for="t in [['flights', 'Flights'], ['drones', 'Drones']]"
:key="t[0]"
class="rounded-md px-3.5 py-1.5 text-sm font-semibold transition"
:class="tab === t[0] ? 'bg-accent-soft text-accent-soft-fg' : 'text-ink-secondary hover:text-ink'"
@click="tab = t[0]"
>
{{ t[1] }}
</button>
</div>
<div class="ml-auto flex items-center gap-2">
<a
:href="exportLogbookUrl()"
class="btn-ghost inline-flex items-center gap-2"
title="Download a compliance CSV (Trafikstyrelsen / police disclosure)"
>
<Icon name="download" :size="15" /> Export CSV
</a>
<button v-if="tab === 'flights'" class="btn-accent inline-flex items-center gap-2" @click="newFlight">
<Icon name="plus" :size="15" /> Log flight
</button>
<button v-else class="btn-accent inline-flex items-center gap-2" @click="newDrone">
<Icon name="plus" :size="15" /> Add drone
</button>
</div>
</div>
<!-- stat row -->
<div class="grid grid-cols-4 gap-4 max-[900px]:grid-cols-2">
<div v-for="s in [
{ label: 'Flights logged', value: stats.total, tone: 'neutral' },
{ label: 'Require logbook', value: stats.required, tone: 'neutral' },
{ label: 'Compliance flags', value: stats.flagged, tone: stats.flagged ? 'danger' : 'success' },
{ label: 'Registered drones', value: stats.fleet, tone: 'neutral' },
]" :key="s.label" class="panel p-5">
<div class="eyebrow">{{ s.label }}</div>
<div class="mt-2 text-[30px] font-bold leading-none tracking-tightest"
:class="s.tone === 'danger' ? 'text-danger-fg' : s.tone === 'success' ? 'text-success-fg' : 'text-ink'">
{{ s.value }}
</div>
</div>
</div>
<div v-if="loadErr" class="panel border-danger/40 p-4 text-sm text-danger-fg">{{ loadErr }}</div>
<!-- ============ FLIGHTS ============ -->
<template v-if="tab === 'flights'">
<!-- add / edit form -->
<div v-if="showFlightForm" class="panel p-5">
<div class="mb-4 flex items-center justify-between">
<div>
<div class="eyebrow">{{ editingFlightId ? 'Edit entry' : 'New entry' }}</div>
<div class="mt-0.5 text-base font-semibold text-ink">Logbook flight (BEK 1649 §5)</div>
</div>
<button class="btn-icon" @click="cancelFlight"><Icon name="x" :size="16" /></button>
</div>
<div class="grid grid-cols-3 gap-3 max-[760px]:grid-cols-1">
<label class="block">
<span class="eyebrow mb-1 block">Date</span>
<input v-model="flightForm.operationDate" type="date" class="field" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Start</span>
<input v-model="flightForm.startTime" type="time" class="field" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">End</span>
<input v-model="flightForm.endTime" type="time" class="field" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Drone</span>
<select v-model="flightForm.drone" class="field">
<option v-if="!drones.length" value="">— add a drone first —</option>
<option v-for="d in drones" :key="d.id" :value="d.id">{{ d.name }}{{ d.model ? ` · ${d.model}` : '' }}</option>
</select>
</label>
<label class="block">
<span class="eyebrow mb-1 block">Max altitude (m AGL)</span>
<input v-model="flightForm.maxAltitudeAgl" type="number" min="0" class="field" placeholder="120" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Area / route</span>
<input v-model="flightForm.areaRoute" class="field" placeholder="Field N of Roskilde, grid survey" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Remote pilot name</span>
<input v-model="flightForm.pilotName" class="field" placeholder="Full name" />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Certificate ref</span>
<input v-model="flightForm.certificateRef" class="field" placeholder="A2 / STS cert no." />
</label>
<label class="block">
<span class="eyebrow mb-1 block">Logging path</span>
<select v-model="flightForm.loggingPath" class="field">
<option v-for="p in PATHS" :key="p.value" :value="p.value">{{ p.label }}</option>
</select>
</label>
<label class="block">
<span class="eyebrow mb-1 block">Category</span>
<select v-model="flightForm.category" class="field">
<option v-for="c in CATEGORIES" :key="c.value" :value="c.value">{{ c.label }}</option>
</select>
</label>
<label class="block">
<span class="eyebrow mb-1 block">Purpose</span>
<select v-model="flightForm.purpose" class="field">
<option v-for="p in PURPOSES" :key="p.value" :value="p.value">{{ p.label }}</option>
</select>
</label>
<label class="block">
<span class="eyebrow mb-1 block">Authorisation ref</span>
<input v-model="flightForm.authorisationRef" class="field" placeholder="Specific-category ref" />
</label>
</div>
<label class="mt-3 block">
<span class="eyebrow mb-1 block">FDR log URL (automatic path)</span>
<input v-model="flightForm.rawFdrLogUrl" class="field" placeholder="Link to the stored flight-data-recorder export" />
</label>
<button class="mt-4 flex items-center gap-1.5 text-sm font-semibold text-accent" @click="showDetails = !showDetails">
<Icon :name="showDetails ? 'x' : 'plus'" :size="14" /> Operational details (weather, airspace, incidents)
</button>
<div v-if="showDetails" class="mt-3 grid grid-cols-2 gap-3 max-[760px]:grid-cols-1">
<label class="block"><span class="eyebrow mb-1 block">Weather / wind</span>
<input v-model="flightForm.weather" class="field" placeholder="6 m/s NW, CAVOK" /></label>
<label class="block"><span class="eyebrow mb-1 block">Airspace / NOTAM ref</span>
<input v-model="flightForm.airspaceRef" class="field" /></label>
<label class="block"><span class="eyebrow mb-1 block">Observer</span>
<input v-model="flightForm.observer" class="field" /></label>
<label class="block"><span class="eyebrow mb-1 block">Incidents / anomalies</span>
<input v-model="flightForm.incidents" class="field" placeholder="RTH trigger, GPS dropout…" /></label>
<label class="col-span-2 block max-[760px]:col-span-1"><span class="eyebrow mb-1 block">Notes</span>
<textarea v-model="flightForm.notes" rows="2" class="field"></textarea></label>
</div>
<div class="mt-4 flex items-center gap-3">
<button class="btn-accent" :disabled="savingFlight" @click="saveFlight">
{{ savingFlight ? 'Saving…' : editingFlightId ? 'Save changes' : 'Log flight' }}
</button>
<button class="btn-ghost" @click="cancelFlight">Cancel</button>
<span v-if="flightMsg" class="text-sm text-danger-fg">{{ flightMsg }}</span>
</div>
</div>
<!-- flights table -->
<div class="panel overflow-hidden p-0">
<div v-if="loading" class="px-5 py-12 text-center text-sm text-ink-muted">Loading…</div>
<div v-else-if="!flights.length" class="grid place-items-center px-5 py-16 text-center">
<Icon name="book" :size="26" class="text-ink-muted" />
<div class="mt-3 text-sm font-medium text-ink-secondary">No flights logged yet</div>
<div class="mt-1 text-xs text-ink-muted">Log your first operation to start the 5-year retention record.</div>
</div>
<div v-else class="overflow-x-auto">
<table class="w-full border-collapse text-sm">
<thead>
<tr class="text-left">
<th v-for="h in ['Date', 'Drone', 'Area / route', 'Alt', 'Pilot', 'Compliance', '']" :key="h"
class="border-y border-line bg-surface-2 px-5 py-2.5 font-mono text-[10.5px] font-normal uppercase tracking-caps text-ink-muted">
{{ h }}
</th>
</tr>
</thead>
<tbody>
<template v-for="f in flights" :key="f.id">
<tr class="border-b border-line last:border-0" :class="editingFlightId === f.id ? 'bg-accent-soft' : ''">
<td class="whitespace-nowrap px-5 py-3 font-mono text-ink">
{{ (f.operationDate || '').slice(0, 10) }}
<span v-if="f.startTime" class="text-ink-muted">{{ f.startTime }}</span>
</td>
<td class="px-5 py-3 text-ink-secondary">{{ f.droneName || '—' }}</td>
<td class="max-w-[220px] truncate px-5 py-3 text-ink-secondary" :title="f.areaRoute">{{ f.areaRoute || '—' }}</td>
<td class="px-5 py-3 font-mono text-ink-secondary">{{ f.maxAltitudeAgl ? f.maxAltitudeAgl + ' m' : '—' }}</td>
<td class="px-5 py-3 text-ink-secondary">{{ f.pilotName || '—' }}</td>
<td class="px-5 py-3">
<button
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-semibold"
:class="badgeClass[flightBadge(f).tone]"
@click="toggleRow(f.id)"
>
<Icon v-if="flightBadge(f).tone === 'danger'" name="alertTriangle" :size="12" />
<Icon v-else-if="flightBadge(f).tone === 'success'" name="check" :size="12" />
{{ flightBadge(f).label }}
</button>
</td>
<td class="whitespace-nowrap px-5 py-3 text-right">
<template v-if="confirmFlightId === f.id">
<span class="mr-2 text-xs text-ink-muted">Delete?</span>
<button class="btn-ghost mr-1" @click="confirmFlightId = ''">Cancel</button>
<button class="rounded bg-danger px-3 py-1.5 text-sm font-semibold text-white transition hover:brightness-110" @click="removeFlight(f)">Delete</button>
</template>
<template v-else>
<button class="btn-ghost mr-1 inline-flex items-center gap-1" @click="editFlight(f)"><Icon name="sliders" :size="13" /> Edit</button>
<button class="btn-ghost inline-flex items-center gap-1" @click="confirmFlightId = f.id"><Icon name="trash" :size="13" /></button>
</template>
</td>
</tr>
<tr v-if="openRow === f.id" class="border-b border-line bg-surface-2">
<td colspan="7" class="px-5 py-3">
<div class="flex flex-wrap gap-x-8 gap-y-1.5 text-xs">
<span class="text-ink-secondary">Logging path: <b class="text-ink">{{ f.compliance?.loggingPath || '—' }}</b></span>
<span class="text-ink-secondary">Category: <b class="text-ink">{{ f.category || '—' }}</b></span>
<span class="text-ink-secondary">Retain until: <b class="font-mono text-ink">{{ (f.retentionUntil || '').slice(0, 10) || '—' }}</b></span>
<span v-if="f.compliance?.exempt" class="text-ink-secondary">Exempt: <b class="text-ink">{{ f.compliance.exemptReason }}</b></span>
</div>
<ul v-if="(f.compliance?.redFlags || []).length" class="mt-2 space-y-1">
<li v-for="(rf, i) in f.compliance.redFlags" :key="i" class="flex items-start gap-2 text-xs text-danger-fg">
<Icon name="alertTriangle" :size="13" class="mt-px shrink-0" /> {{ rf }}
</li>
</ul>
<div v-else-if="!f.compliance?.exempt" class="mt-2 text-xs text-success-fg">No compliance gaps detected.</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- ============ DRONES ============ -->
<template v-else>
<div v-if="showDroneForm" class="panel p-5">
<div class="mb-4 flex items-center justify-between">
<div>
<div class="eyebrow">{{ editingDroneId ? 'Edit drone' : 'New drone' }}</div>
<div class="mt-0.5 text-base font-semibold text-ink">Aircraft registry</div>
</div>
<button class="btn-icon" @click="cancelDrone"><Icon name="x" :size="16" /></button>
</div>
<div class="grid grid-cols-3 gap-3 max-[760px]:grid-cols-1">
<label class="block"><span class="eyebrow mb-1 block">Name</span>
<input v-model="droneForm.name" class="field" placeholder="Mavic-01" /></label>
<label class="block"><span class="eyebrow mb-1 block">Model</span>
<input v-model="droneForm.model" class="field" placeholder="DJI Mavic 3 Enterprise" /></label>
<label class="block"><span class="eyebrow mb-1 block">Serial</span>
<input v-model="droneForm.serial" class="field" /></label>
<label class="block"><span class="eyebrow mb-1 block">Operator no.</span>
<input v-model="droneForm.operatorNumber" class="field" placeholder="DNK…" /></label>
<label class="block"><span class="eyebrow mb-1 block">MTOM (grams)</span>
<input v-model="droneForm.mtomGrams" type="number" min="0" class="field" placeholder="920" /></label>
<label class="block"><span class="eyebrow mb-1 block">C-class</span>
<select v-model="droneForm.cClass" class="field">
<option v-for="c in C_CLASSES" :key="c" :value="c">{{ c || '— none —' }}</option>
</select></label>
</div>
<div class="mt-3 flex flex-wrap gap-6">
<label class="flex items-center gap-2 text-sm text-ink-secondary">
<input v-model="droneForm.autologsFlights" type="checkbox" class="h-4 w-4 accent-[var(--accent)]" />
Auto-logs flights (onboard FDR)
</label>
<label class="flex items-center gap-2 text-sm text-ink-secondary">
<input v-model="droneForm.isToy" type="checkbox" class="h-4 w-4 accent-[var(--accent)]" />
Toy drone (logbook-exempt)
</label>
</div>
<div class="mt-4 flex items-center gap-3">
<button class="btn-accent" :disabled="savingDrone" @click="saveDrone">
{{ savingDrone ? 'Saving…' : editingDroneId ? 'Save changes' : 'Add drone' }}
</button>
<button class="btn-ghost" @click="cancelDrone">Cancel</button>
<span v-if="droneMsg" class="text-sm text-danger-fg">{{ droneMsg }}</span>
</div>
</div>
<div class="panel overflow-hidden p-0">
<div v-if="loading" class="px-5 py-12 text-center text-sm text-ink-muted">Loading…</div>
<div v-else-if="!drones.length" class="grid place-items-center px-5 py-16 text-center">
<Icon name="drone" :size="26" class="text-ink-muted" />
<div class="mt-3 text-sm font-medium text-ink-secondary">No drones registered</div>
<div class="mt-1 text-xs text-ink-muted">Register the airframes you fly to log flights against them.</div>
</div>
<div v-else class="overflow-x-auto">
<table class="w-full border-collapse text-sm">
<thead>
<tr class="text-left">
<th v-for="h in ['Name', 'Model', 'MTOM', 'Class', 'FDR', '']" :key="h"
class="border-y border-line bg-surface-2 px-5 py-2.5 font-mono text-[10.5px] font-normal uppercase tracking-caps text-ink-muted">
{{ h }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="d in drones" :key="d.id" class="border-b border-line last:border-0" :class="editingDroneId === d.id ? 'bg-accent-soft' : ''">
<td class="px-5 py-3 font-semibold text-ink">{{ d.name }}</td>
<td class="px-5 py-3 text-ink-secondary">{{ d.model || '—' }}</td>
<td class="px-5 py-3 font-mono text-ink-secondary">{{ d.mtomGrams ? d.mtomGrams + ' g' : '—' }}</td>
<td class="px-5 py-3">
<span v-if="d.cClass" class="inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold" :class="badgeClass.accent">{{ d.cClass }}</span>
<span v-else class="text-ink-muted">—</span>
<span v-if="d.isToy" class="ml-1 inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold" :class="badgeClass.neutral">toy</span>
</td>
<td class="px-5 py-3">
<span class="text-xs" :class="d.autologsFlights ? 'text-success-fg' : 'text-ink-muted'">{{ d.autologsFlights ? 'yes' : 'no' }}</span>
</td>
<td class="whitespace-nowrap px-5 py-3 text-right">
<template v-if="confirmDroneId === d.id">
<span class="mr-2 text-xs text-ink-muted">Delete?</span>
<button class="btn-ghost mr-1" @click="confirmDroneId = ''">Cancel</button>
<button class="rounded bg-danger px-3 py-1.5 text-sm font-semibold text-white transition hover:brightness-110" @click="removeDrone(d)">Delete</button>
</template>
<template v-else>
<button class="btn-ghost mr-1 inline-flex items-center gap-1" @click="editDrone(d)"><Icon name="sliders" :size="13" /> Edit</button>
<button class="btn-ghost inline-flex items-center gap-1" @click="confirmDroneId = d.id"><Icon name="trash" :size="13" /></button>
</template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>