30 — SQL Console (Fullscreen)
Power-user surface. Monaco editor + DuckDB-Wasm + result table for
ad-hoc analytics over cached session Parquets. Reached from
16-analysis-hub.md's ◆ Start.
Purpose
Verb: Hack. Run arbitrary SQL against the cached telemetry.
Power users want answers their dashboard doesn't expose.
Wireframe
┌────────────────────────────────────────────────────────────┐
│ TAHA · LV.12 · ⚙ T-ROD · ░ DUCKDB ON 15:32 PT │
│ ───────────────────────────────────────────────────────── │
│ SQL CONSOLE │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ -- Where did I lose time at T7 best vs second-best │ │
│ │ SELECT distance_m, speed_best * 3.6 AS speed_best, │ │
│ │ speed_2nd * 3.6 AS speed_2nd, │ │
│ │ (speed_2nd - speed_best) * 3.6 AS delta_kmh │ │
│ │ FROM compare_laps('sonoma-001', 7, 5) │ │
│ │ WHERE distance_m BETWEEN 1620 AND 1820 │ │
│ │ ORDER BY delta_kmh DESC LIMIT 10;█ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ▶ RUN ☆ SAVE 📂 LOAD 🗑 CLEAR │
│ │
│ RESULT 10 rows · 0.4 s │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ distance_m │ speed_best │ speed_2nd │ delta_kmh │ │
│ │ 1720 │ 92.1 │ 99.4 │ +7.3 │ │
│ │ 1715 │ 91.8 │ 98.2 │ +6.4 │ │
│ │ … │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────┐ │
│ │T-ROD │ (idle in corner, emotion = analyzing) │
│ │analyz│ │
│ └──────┘ │
│ │
│ A · RUN B · BACK ◆ EXAMPLES │
└────────────────────────────────────────────────────────────┘
States
| State |
Trigger |
Behaviour |
editor-focused |
Mount |
Cursor in Monaco; DuckDB-Wasm warming up if not already |
running |
RUN button or A |
Query executes; coach analyzing |
result |
Query done |
Result table populates; row count + duration shown |
error |
Query syntax error or runtime |
Red highlight on Monaco line + error in result panel |
saving |
SAVE pressed |
Prompt for query name; persist to save slot |
loading-saved |
LOAD pressed |
List of saved queries; cursor picks one |
Sprite usage
| Sprite |
Where |
Animation |
Coach (save.preferredCoach) |
Bottom-right, 48×48 |
Emotion = intense (editor focused) → analyzing (running) → encouraging (result OK) → concerned (error) |
frame-default |
Editor + result panels |
Static |
loading_dots |
While running |
6-frame spin |
Vue component
<!-- pitwall-web/src/views/SqlConsoleFullscreen.vue -->
<template>
<div class="viewport">
<StatusBar :extra="`░ DUCKDB ${duck.ready ? 'ON' : 'BOOTING…'}`" />
<h1 class="font-title text-title">SQL CONSOLE</h1>
<MonacoEditor v-model="query" language="sql" :options="editorOpts" />
<Toolbar>
<Button @click="run" :loading="running">▶ RUN</Button>
<Button @click="save">☆ SAVE</Button>
<Button @click="load">📂 LOAD</Button>
<Button @click="clear">🗑 CLEAR</Button>
</Toolbar>
<ResultPanel v-if="result" :result="result" />
<ErrorPanel v-if="error" :error="error" />
<Sprite :sheet="save.preferredCoach" :animation="emotion" :variant="'idle'" />
<HintBar :hints="hints" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useDuckDBStore } from '@/stores/duckdb'
import { useSavedQueries } from '@/lib/saved-queries'
const duck = useDuckDBStore()
const query = ref(EXAMPLE_QUERIES[0].sql)
const result = ref<any | null>(null)
const error = ref<string | null>(null)
const running = ref(false)
const emotion = computed(() => {
if (running.value) return 'analyzing'
if (error.value) return 'concerned'
if (result.value) return 'encouraging'
return 'intense'
})
async function run() {
running.value = true; error.value = null
try {
result.value = await duck.query(query.value)
} catch (e) {
error.value = String(e); result.value = null
} finally {
running.value = false
}
}
</script>
Pre-loaded example queries
| Title |
Use case |
| Last session lap times |
one row per lap with sector splits |
| Where I lost time at T7 |
speed-trace diff between best + 2nd-best |
| Top 5 laps across the season |
best 5 laps by lap time |
| Trail-brake quality across corners |
brake>5 bar AND |
| Coast time per straight |
time spent off-throttle on each named straight |
| Personal best evolution |
best lap per session over time |
These ship as static defaults; user can SAVE additional queries per
save slot.
Endpoints consumed
| Endpoint |
Use |
GET /session/<sid>/export.parquet?table=telemetry |
Hydrates DuckDB-Wasm with the selected session's parquet |
GET /session/<sid>/export.parquet?table=telemetry_signals |
Tall-store parquet for ADR-015 signals |
After hydration: pure DuckDB-Wasm. No bridge calls per query.
Audio cues
| Event |
Sound |
| RUN |
cursor_select |
| Query OK |
goal_complete |
| Query error |
error_quiet |
| SAVE |
cursor_select |
| LOAD |
cursor_select |
| Input |
Action |
| (Monaco focused) typing |
Edit query |
| Cmd/Ctrl+Enter |
Run |
| Esc |
Blur Monaco |
| A (when blurred) |
Run |
| B |
Back to Analysis Hub |
| Start |
Pause menu |
| ◆ |
Open EXAMPLES dropdown |
Edge cases
- DuckDB-Wasm fails to load — show error + fallback message; suggest
using individual analytics screens (17-22) which call bridge directly
- Query runs > 5 s — show running indicator; user can abort via
another RUN press (kills + restarts) or B
- Query result > 10 000 rows — paginate (1000 per page); export
to CSV button surfaces in result toolbar
- Saved queries quota — capped at 20 per save slot; oldest evicted
on overflow