mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
use 6 digit codes for bridge
This commit is contained in:
@@ -22,11 +22,60 @@
|
||||
|
||||
<div
|
||||
v-if="!hasToken"
|
||||
class="mt-10 rounded-3xl border border-red-500/30 bg-red-500/10 px-6 py-10 text-center shadow-[0_25px_80px_rgba(64,18,18,0.35)] sm:px-8"
|
||||
class="mt-10 rounded-3xl border border-[#16BBD7]/35 bg-gradient-to-b from-[#122145]/95 to-[#0B132A]/95 px-6 py-10 text-center shadow-[0_25px_80px_rgba(8,25,60,0.55)] sm:px-8"
|
||||
>
|
||||
<h2 class="text-2xl font-semibold">Open from the Bridge app</h2>
|
||||
<p class="mt-4 text-sm text-red-100/80">Launch the desktop Bridge and tap the “Link account” button so we can pass the code over automatically.</p>
|
||||
<p class="mt-6 text-xs uppercase tracking-[0.38em] text-red-200/60">Waiting for desktop link</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.38em] text-[#72d9ea]/85">Manual pairing</p>
|
||||
<h2 class="mt-3 text-2xl font-semibold sm:text-3xl">Enter your pairing code</h2>
|
||||
<p class="mt-4 text-sm text-white/75">
|
||||
No auto-link available? Enter the 6-character code shown in the Bridge app.
|
||||
</p>
|
||||
|
||||
<form class="mx-auto mt-8 max-w-xl space-y-5 text-left" @submit.prevent="applyPairingCode">
|
||||
<div class="rounded-3xl border border-[#16BBD7]/40 bg-[#081129]/80 p-4 shadow-inner shadow-[#16BBD7]/15">
|
||||
<label for="pairing-code" class="block text-xs font-semibold uppercase tracking-[0.28em] text-[#9be6f2]/80">
|
||||
Pairing code
|
||||
</label>
|
||||
<input
|
||||
id="pairing-code"
|
||||
v-model="manualTokenInput"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocapitalize="characters"
|
||||
autocomplete="one-time-code"
|
||||
spellcheck="false"
|
||||
maxlength="6"
|
||||
placeholder="A1B2C3"
|
||||
class="mt-3 w-full rounded-2xl border border-[#16BBD7]/55 bg-[#050c1f] px-4 py-4 text-center font-mono text-3xl font-semibold uppercase tracking-[0.45em] text-[#9be6f2] placeholder:text-[#9be6f2]/35 focus:border-[#72d9ea] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea]/45"
|
||||
@input="onManualTokenInput"
|
||||
>
|
||||
<p class="mt-3 text-xs uppercase tracking-[0.26em] text-white/55">A-Z and 0-9 only</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-6 gap-2">
|
||||
<div
|
||||
v-for="(char, index) in manualTokenSlots"
|
||||
:key="`manual-token-slot-${index}`"
|
||||
class="flex h-11 items-center justify-center rounded-xl border border-white/15 bg-[#0B132A]/75 font-mono text-base font-semibold text-white/85"
|
||||
>
|
||||
{{ char || '•' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs uppercase tracking-[0.32em]" :class="manualTokenReady ? 'text-emerald-200/90' : 'text-white/60'">
|
||||
{{ manualTokenReady ? 'Code complete' : `${manualTokenRemaining} characters missing` }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center rounded-2xl bg-[#16BBD7] px-5 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea] disabled:cursor-not-allowed disabled:bg-[#16BBD7]/55"
|
||||
:disabled="!manualTokenReady"
|
||||
>
|
||||
Continue with pairing code
|
||||
</button>
|
||||
<p v-if="manualTokenError" class="text-sm text-amber-200">{{ manualTokenError }}</p>
|
||||
</form>
|
||||
|
||||
<p class="mt-6 text-center text-xs uppercase tracking-[0.38em] text-white/45">Waiting for desktop link</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
@@ -234,7 +283,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useHead, useRoute } from '#imports'
|
||||
import { useHead, useRoute, useRouter } from '#imports'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
interface BridgeStatusPayload {
|
||||
@@ -260,6 +309,7 @@ interface BridgeLivePayload {
|
||||
useHead({ title: 'Link Bridge · OpenSquawk' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const { user, accessToken, isAuthenticated, initialized } = storeToRefs(auth)
|
||||
|
||||
@@ -279,16 +329,23 @@ const liveTelemetryError = ref('')
|
||||
const liveTelemetryRequestActive = ref(false)
|
||||
|
||||
const copiedToken = ref(false)
|
||||
const manualTokenInput = ref('')
|
||||
const manualTokenError = ref('')
|
||||
|
||||
const token = computed(() => {
|
||||
const value = route.query.token
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] || ''
|
||||
}
|
||||
return typeof value === 'string' ? value : ''
|
||||
const rawToken = Array.isArray(value)
|
||||
? value[0] || ''
|
||||
: typeof value === 'string'
|
||||
? value
|
||||
: ''
|
||||
return rawToken.trim()
|
||||
})
|
||||
|
||||
const hasToken = computed(() => token.value.length > 0)
|
||||
const hasToken = computed(() => token.value.length >= 6)
|
||||
const manualTokenSlots = computed(() => Array.from({ length: 6 }, (_unused, index) => manualTokenInput.value[index] || ''))
|
||||
const manualTokenRemaining = computed(() => Math.max(0, 6 - manualTokenInput.value.length))
|
||||
const manualTokenReady = computed(() => manualTokenRemaining.value === 0)
|
||||
const loginTarget = computed(() => `/login?redirect=${encodeURIComponent(route.fullPath || '/bridge')}`)
|
||||
|
||||
const authDisplayName = computed(() => {
|
||||
@@ -323,6 +380,34 @@ const liveTelemetryJson = computed(() => {
|
||||
|
||||
const successBannerVisible = computed(() => connectSuccess.value || Boolean(connectionStatus.value?.connected))
|
||||
|
||||
function sanitizePairingCode(input: string) {
|
||||
return input.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6)
|
||||
}
|
||||
|
||||
function onManualTokenInput() {
|
||||
manualTokenInput.value = sanitizePairingCode(manualTokenInput.value)
|
||||
if (manualTokenError.value && manualTokenReady.value) {
|
||||
manualTokenError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPairingCode() {
|
||||
manualTokenInput.value = sanitizePairingCode(manualTokenInput.value)
|
||||
if (!manualTokenReady.value) {
|
||||
manualTokenError.value = 'Please enter a valid 6-character pairing code.'
|
||||
return
|
||||
}
|
||||
|
||||
manualTokenError.value = ''
|
||||
connectError.value = ''
|
||||
await router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
token: manualTokenInput.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function formatTimestamp(value: string | null) {
|
||||
if (!value) {
|
||||
return '—'
|
||||
|
||||
@@ -18,17 +18,22 @@ import type { FlightLabTelemetryState } from '../../../shared/data/flightlab/typ
|
||||
function mapBridgeTelemetry(raw: Record<string, any>): FlightLabTelemetryState {
|
||||
return {
|
||||
AIRSPEED_INDICATED: raw.ias_kt ?? raw.AIRSPEED_INDICATED ?? 0,
|
||||
AIRSPEED_TRUE: raw.tas_kt ?? raw.AIRSPEED_TRUE ?? 0,
|
||||
GROUND_VELOCITY: raw.groundspeed_kt ?? raw.GROUND_VELOCITY ?? 0,
|
||||
VERTICAL_SPEED: raw.vertical_speed_fpm ?? raw.VERTICAL_SPEED ?? 0,
|
||||
PLANE_ALTITUDE: raw.altitude_ft_indicated ?? raw.altitude_ft_true ?? raw.PLANE_ALTITUDE ?? 0,
|
||||
PLANE_PITCH_DEGREES: raw.pitch_deg ?? raw.PLANE_PITCH_DEGREES ?? 0,
|
||||
TURB_ENG_N1_1: raw.n1_pct ?? raw.TURB_ENG_N1_1 ?? 0,
|
||||
TURB_ENG_N1_2: raw.n1_pct_2 ?? raw.TURB_ENG_N1_2 ?? 0,
|
||||
ENG_COMBUSTION: !!(raw.eng_on ?? raw.ENG_COMBUSTION ?? false),
|
||||
SIM_ON_GROUND: !!(raw.on_ground ?? raw.SIM_ON_GROUND ?? false),
|
||||
GEAR_HANDLE_POSITION: !!(raw.gear_handle ?? raw.GEAR_HANDLE_POSITION ?? false),
|
||||
FLAPS_HANDLE_INDEX: raw.flaps_index ?? raw.FLAPS_HANDLE_INDEX ?? 0,
|
||||
BRAKE_PARKING_POSITION: !!(raw.parking_brake ?? raw.BRAKE_PARKING_POSITION ?? false),
|
||||
AUTOPILOT_MASTER: !!(raw.autopilot_master ?? raw.AUTOPILOT_MASTER ?? false),
|
||||
TRANSPONDER_CODE: raw.transponder_code ?? raw.TRANSPONDER_CODE ?? 0,
|
||||
ADF_ACTIVE_FREQUENCY: raw.adf_active_freq ?? raw.ADF_ACTIVE_FREQUENCY ?? 0,
|
||||
ADF_STANDBY_FREQUENCY: raw.adf_standby_freq_hz ?? raw.ADF_STANDBY_FREQUENCY ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +59,11 @@ export default defineEventHandler(async (event) => {
|
||||
const mapped = mapBridgeTelemetry(body)
|
||||
flightlabTelemetryStore.update(userId, mapped)
|
||||
|
||||
event.node.res.statusCode = 204
|
||||
return {
|
||||
// n1_pct: (mapped.TURB_ENG_N1_1 ?? 0) + 10,
|
||||
// n1_pct_2: (mapped.TURB_ENG_N1_2 ?? 0) + 10,
|
||||
// gear und parking break togglen
|
||||
// gear_handle: !mapped.GEAR_HANDLE_POSITION,
|
||||
parking_brake: !mapped.BRAKE_PARKING_POSITION,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ export function normalizeBridgeToken(input: unknown) {
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
if (token.length < 8 || token.length > 256) {
|
||||
if (token.length < 6 || token.length > 256) {
|
||||
return null
|
||||
}
|
||||
return token
|
||||
|
||||
@@ -20,18 +20,23 @@ export interface FlightLabButton {
|
||||
|
||||
/** MSFS2020 SimConnect variable state — keys match SimConnect naming */
|
||||
export interface FlightLabTelemetryState {
|
||||
AIRSPEED_INDICATED: number // knots
|
||||
AIRSPEED_INDICATED: number // knots (IAS)
|
||||
AIRSPEED_TRUE: number // knots (TAS)
|
||||
GROUND_VELOCITY: number // knots
|
||||
VERTICAL_SPEED: number // feet per minute
|
||||
PLANE_ALTITUDE: number // feet MSL
|
||||
PLANE_PITCH_DEGREES: number // degrees
|
||||
TURB_ENG_N1_1: number // percent (0-100), engine 1
|
||||
TURB_ENG_N1_2: number // percent (0-100), engine 2
|
||||
ENG_COMBUSTION: boolean // true = at least one engine running
|
||||
SIM_ON_GROUND: boolean
|
||||
GEAR_HANDLE_POSITION: boolean // true = down, false = up
|
||||
FLAPS_HANDLE_INDEX: number // 0-4 for A320
|
||||
BRAKE_PARKING_POSITION: boolean // true = set, false = released
|
||||
AUTOPILOT_MASTER: boolean
|
||||
TRANSPONDER_CODE: number // squawk code (0-7777 octal)
|
||||
ADF_ACTIVE_FREQUENCY: number // Hz
|
||||
ADF_STANDBY_FREQUENCY: number // Hz
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user