diff --git a/CLAUDE.md b/CLAUDE.md
index e02e9c4..1d30d19 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
diff --git a/app/app.vue b/app/app.vue
index 967eb08..ea3a522 100644
--- a/app/app.vue
+++ b/app/app.vue
@@ -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;
};
diff --git a/app/components/CookieConsentBanner.vue b/app/components/CookieConsentBanner.vue
index 2e6cdd1..8c09589 100644
--- a/app/components/CookieConsentBanner.vue
+++ b/app/components/CookieConsentBanner.vue
@@ -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);
diff --git a/app/composables/usePmTheme.ts b/app/composables/usePmTheme.ts
index 1756e31..01bef07 100644
--- a/app/composables/usePmTheme.ts
+++ b/app/composables/usePmTheme.ts
@@ -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 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
, 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 }
}
diff --git a/app/pages/classroom-introduction.vue b/app/pages/classroom-introduction.vue
index 152323e..0333534 100644
--- a/app/pages/classroom-introduction.vue
+++ b/app/pages/classroom-introduction.vue
@@ -73,25 +73,25 @@