atc header

This commit is contained in:
itsrubberduck
2026-07-10 08:33:36 +02:00
parent 9014955ce2
commit 9f0a51e678
10 changed files with 6649 additions and 6576 deletions

View File

@@ -6,7 +6,7 @@
- **Shared types/utils** in `/shared`
- MongoDB models in `/server/models`
- **Python backend** — a separate repo, `OpenSquawk-LiveATC-api`, owns the authoritative
Live ATC (`/pm`) session state and routing decisions. It runs as its own service
Live ATC (`/live-atc`, formerly `/pm` — legacy path now redirects) session state and routing decisions. It runs as its own service
(default `http://127.0.0.1:8000`). This Nuxt app owns STT, TTS, client audio, auth,
the flow editor, and account/admin features.
@@ -16,21 +16,21 @@
## Key Files
- `/app/composables/useRadioBackend.ts` — Typed client for the Python backend REST API
(`createSession`, `transmit`, `deleteSession`, `fetchFlows`). This is how `/pm` talks
(`createSession`, `transmit`, `deleteSession`, `fetchFlows`). This is how `/live-atc` talks
to the decision engine.
- `/shared/utils/communicationsEngine.ts` — Local state-machine composable used by `/pm`.
- `/shared/utils/communicationsEngine.ts` — Local state-machine composable used by `/live-atc`.
It mirrors the flow for cursor tracking and TTS scheduling; the **Python backend owns the
authoritative state**. `fetchRuntimeTree()` loads the flow definition from the backend's
`/api/decision-flows/runtime`.
- `/server/services/decisionFlowService.ts` — Builds/edits MongoDB-backed decision trees.
Used **only by the flow editor** (`/server/api/editor/flows*`), not by `/pm`.
Used **only by the flow editor** (`/server/api/editor/flows*`), not by `/live-atc`.
- `/server/utils/openai.ts``getOpenAIClient()` only. Used for TTS (`/api/atc/say`) and
STT (`/api/atc/ptt`). It is **not** a decision router anymore.
- `/app/pages/pm.vue` — Live ATC page (speech-to-text, PTT, text input).
- `/app/pages/live-atc.vue` — Live ATC page (speech-to-text, PTT, text input).
- `/app/pages/classroom.vue` — Classroom learning mode (separate system, does NOT use
communicationsEngine).
## Live ATC Flow (/pm)
## Live ATC Flow (/live-atc)
1. `startMonitoring()``fetchRuntimeTree('icao_atc_decision_tree', radioBackendUrl)`
loads the flow definition from the Python backend into the local engine (cursor / TTS).
2. `startMonitoring()``radioBackend.createSession(...)` creates the authoritative

View File

@@ -79,7 +79,7 @@ const scheduleHotjarInitialization = () => {
const resolveTrackedProduct = (path: string): 'classroom' | 'liveatc' | null => {
if (path.startsWith('/classroom')) return 'classroom';
if (path.startsWith('/pm') || path.startsWith('/copilot') || path.startsWith('/bridge')) return 'liveatc';
if (path.startsWith('/live-atc') || path.startsWith('/pm') || path.startsWith('/copilot') || path.startsWith('/bridge')) return 'liveatc';
return null;
};

View File

@@ -110,7 +110,7 @@ watch(
}
);
const restrictedRoute = computed(() => route.path.startsWith('/classroom') || route.path.startsWith('/pm') || route.path.startsWith('/login'));
const restrictedRoute = computed(() => route.path.startsWith('/classroom') || route.path.startsWith('/live-atc') || route.path.startsWith('/pm') || route.path.startsWith('/login'));
const showManageButton = computed(() => hasConsent.value && !isDialogVisible.value && !restrictedRoute.value);

View File

@@ -8,11 +8,12 @@ export type PmEffectiveTheme = 'light' | 'dark'
const PM_THEME_COOKIE = 'os_pm_theme'
/**
* /pm-scoped light/dark toggle. Defaults to dark (matching the rest of the app)
* /live-atc-scoped light/dark toggle (formerly /pm — cookie name kept for
* backwards compatibility). Defaults to dark (matching the rest of the app)
* until the user explicitly picks Light or System, at which point that choice is
* remembered and wins over the default. Only /pm has a light theme today — the
* Vuetify theme is reset back to the app-wide dark default on unmount so other
* pages are unaffected.
* remembered and wins over the default. Only /live-atc has a light theme today —
* the Vuetify theme is reset back to the app-wide dark default on unmount so
* other pages are unaffected.
*/
export function usePmTheme() {
const vuetifyTheme = useTheme()
@@ -41,6 +42,16 @@ export function usePmTheme() {
storedPreference.value = next
}
// Mirrors effectiveTheme onto <html data-theme="..."> so the --bg/--text/--border
// CSS variables (defined in global.css) reach content that escapes .pm-page in the
// DOM — Vuetify teleports menus, dialogs, and tooltips to <body>, where they'd
// otherwise only see the app-wide dark defaults from :root.
function applyDocumentTheme() {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', effectiveTheme.value)
}
}
if (typeof window !== 'undefined') {
const media = window.matchMedia('(prefers-color-scheme: light)')
systemPrefersLight.value = media.matches
@@ -53,10 +64,14 @@ export function usePmTheme() {
onBeforeUnmount(() => {
media.removeEventListener('change', handleChange)
vuetifyTheme.global.name.value = 'opensquawkDark'
document.documentElement.removeAttribute('data-theme')
})
}
watch(effectiveTheme, applyVuetifyTheme, { immediate: true })
watch(effectiveTheme, () => {
applyVuetifyTheme()
applyDocumentTheme()
}, { immediate: true })
return { preference, effectiveTheme, setPreference }
}

View File

@@ -73,25 +73,25 @@
<div class="mt-6 space-y-4">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/70">Delivery</p>
<div class="mt-3 flex flex-wrap gap-2">
<div class="delivery-seg mt-3">
<button
type="button"
class="btn soft voice-toggle"
:class="{ active: voiceMode === 'text' }"
class="delivery-seg-btn"
:class="{ 'is-active': voiceMode === 'text' }"
@click="setVoiceMode('text')"
:aria-pressed="voiceMode === 'text'"
>
<v-icon icon="mdi-text" size="18" class="text-cyan-200" />
<v-icon icon="mdi-text" size="18" />
Text only
</button>
<button
type="button"
class="btn soft voice-toggle"
:class="{ active: voiceMode === 'radio' }"
class="delivery-seg-btn"
:class="{ 'is-active': voiceMode === 'radio' }"
@click="setVoiceMode('radio')"
:aria-pressed="voiceMode === 'radio'"
>
<v-icon icon="mdi-radio-handheld" size="18" class="text-cyan-200" />
<v-icon icon="mdi-radio-handheld" size="18" />
Radio voice
</button>
</div>
@@ -916,23 +916,39 @@ onBeforeUnmount(() => {
</script>
<style scoped>
.voice-toggle {
transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
/* Single pill instead of two competing buttons — this is a delivery
preference, not a second call to action next to "Start guided tour". */
.delivery-seg {
display: flex;
gap: 4px;
padding: 4px;
border-radius: 14px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--text) 4%, transparent);
}
.voice-toggle .v-icon {
color: color-mix(in srgb, var(--accent) 70%, white 30%);
.delivery-seg-btn {
flex: 1 1 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 9px 12px;
border-radius: 10px;
font-size: 13.5px;
font-weight: 600;
color: var(--t3);
transition: color 130ms ease, background 130ms ease;
-webkit-tap-highlight-color: transparent;
}
.voice-toggle.active {
background: color-mix(in srgb, var(--accent) 14%, transparent);
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
color: var(--text);
box-shadow: 0 12px 28px color-mix(in srgb, var(--accent) 26%, transparent);
.delivery-seg-btn:hover {
color: color-mix(in srgb, var(--text) 85%, transparent);
}
.voice-toggle.active .v-icon {
color: color-mix(in srgb, var(--accent) 85%, white 15%);
.delivery-seg-btn.is-active {
color: #050910;
background: #22d3ee;
}
.voice-replay {

View File

@@ -1931,7 +1931,7 @@ const experiences: ExperienceOption[] = [
icon: 'mdi-radio-handheld',
to: '/bridge',
target: '_blank',
matches: path => path.startsWith('/pm')
matches: path => path.startsWith('/live-atc') || path.startsWith('/pm')
}
]

6573
app/pages/live-atc.vue Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@
<NuxtLink
v-if="liveAtcUnlocked"
to="/pm"
to="/live-atc"
class="relative flex h-full flex-col gap-3 overflow-hidden rounded-2xl border border-amber-400/30 bg-amber-400/[0.06] p-5 transition hover:border-amber-400/50 hover:bg-amber-400/10"
>
<svg

View File

@@ -60,7 +60,7 @@ export default defineNuxtConfig({
radioBackendUrl: process.env.NUXT_PUBLIC_RADIO_BACKEND_URL || 'http://127.0.0.1:8000',
// Minimum word count for a voice (PTT) transmission to be used. Below
// this the transcript is treated as STT noise/hallucination and
// ignored. See the "STT MINIMUM-WORD GATE" in pages/pm.vue.
// ignored. See the "STT MINIMUM-WORD GATE" in pages/live-atc.vue.
pttMinWords: Number(process.env.NUXT_PUBLIC_PTT_MIN_WORDS || 2),
},
},
@@ -85,7 +85,7 @@ export default defineNuxtConfig({
'on-surface': '#ffffff'
}
},
// Only used on /pm today (see app/composables/usePmTheme.ts) — not the app default.
// Only used on /live-atc today (see app/composables/usePmTheme.ts) — not the app default.
opensquawkLight: {
dark: false,
colors: {