mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
UI Updates and Web App
This commit is contained in:
@@ -1616,34 +1616,14 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
|
|||||||
tlaStr := tlaStyle.Render(padRight(tla, 4))
|
tlaStr := tlaStyle.Render(padRight(tla, 4))
|
||||||
tyreStr := m.renderTyreIndicator(d.RacingNumber)
|
tyreStr := m.renderTyreIndicator(d.RacingNumber)
|
||||||
|
|
||||||
if d.Retired {
|
|
||||||
row := fmt.Sprintf(" %s %s %s %s %s %s",
|
|
||||||
posStr, deltaStr, colorBar, tlaStr,
|
|
||||||
styleDNF.Render("RET"), styleMuted.Render("Retired"))
|
|
||||||
if idx == m.cursor {
|
|
||||||
return styleSelected.Render(row)
|
|
||||||
}
|
|
||||||
if d.KnockedOut {
|
|
||||||
return styleMuted.Render(row)
|
|
||||||
}
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.InPit {
|
|
||||||
row := fmt.Sprintf(" %s %s %s %s %s %s %s",
|
|
||||||
posStr, deltaStr, colorBar, tlaStr, tyreStr,
|
|
||||||
styleSafetyCar.Render(padRight("PIT", 10)),
|
|
||||||
m.renderGapStr(d, fpq))
|
|
||||||
if idx == m.cursor {
|
|
||||||
return styleSelected.Render(row)
|
|
||||||
}
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last lap time coloring
|
// Last lap time coloring
|
||||||
lastLapRaw := d.LastLapTime
|
lastLapRaw := d.LastLapTime
|
||||||
lastLap := padRight(lastLapRaw, 10)
|
lastLap := padRight(lastLapRaw, 10)
|
||||||
if lastLapRaw != "" {
|
if d.Retired {
|
||||||
|
lastLap = padRightVisible(styleDNF.Render("RET"), 10)
|
||||||
|
} else if d.InPit {
|
||||||
|
lastLap = padRightVisible(styleSafetyCar.Render("PIT"), 10)
|
||||||
|
} else if lastLapRaw != "" {
|
||||||
if d.LastLapOB {
|
if d.LastLapOB {
|
||||||
lastLap = padRightVisible(stylePurple.Render(lastLapRaw), 10)
|
lastLap = padRightVisible(stylePurple.Render(lastLapRaw), 10)
|
||||||
} else if d.LastLapPB {
|
} else if d.LastLapPB {
|
||||||
@@ -1652,6 +1632,11 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var row string
|
var row string
|
||||||
|
gapStr := m.renderGapStr(d, fpq)
|
||||||
|
if d.Retired {
|
||||||
|
gapStr = padRightVisible(styleMuted.Render("Retired"), 10)
|
||||||
|
}
|
||||||
|
|
||||||
if m.showSectors {
|
if m.showSectors {
|
||||||
timeCol := lastLap
|
timeCol := lastLap
|
||||||
if fpq {
|
if fpq {
|
||||||
@@ -1663,7 +1648,7 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
|
|||||||
m.renderSector(d.Sectors[0]),
|
m.renderSector(d.Sectors[0]),
|
||||||
m.renderSector(d.Sectors[1]),
|
m.renderSector(d.Sectors[1]),
|
||||||
m.renderSector(d.Sectors[2]),
|
m.renderSector(d.Sectors[2]),
|
||||||
m.renderGapStr(d, fpq))
|
gapStr)
|
||||||
} else if fpq {
|
} else if fpq {
|
||||||
// FP / Qualifying: show BEST lap as primary, LAST as secondary
|
// FP / Qualifying: show BEST lap as primary, LAST as secondary
|
||||||
bestLap := m.renderBestLapTime(d, overallBest)
|
bestLap := m.renderBestLapTime(d, overallBest)
|
||||||
@@ -1677,7 +1662,7 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
|
|||||||
flyingIndicator,
|
flyingIndicator,
|
||||||
bestLap,
|
bestLap,
|
||||||
lastLap,
|
lastLap,
|
||||||
m.renderGapStr(d, fpq))
|
gapStr)
|
||||||
// Highlight danger zone (cutoff) in qualifying
|
// Highlight danger zone (cutoff) in qualifying
|
||||||
if d.Cutoff && idx != m.cursor {
|
if d.Cutoff && idx != m.cursor {
|
||||||
row = lipgloss.NewStyle().Foreground(lipgloss.Color(colorOrange)).Render(row)
|
row = lipgloss.NewStyle().Foreground(lipgloss.Color(colorOrange)).Render(row)
|
||||||
@@ -1692,11 +1677,13 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
|
|||||||
posStr, deltaStr, colorBar, tlaStr, tyreStr,
|
posStr, deltaStr, colorBar, tlaStr, tyreStr,
|
||||||
m.renderTyreAge(d.RacingNumber),
|
m.renderTyreAge(d.RacingNumber),
|
||||||
lastLap,
|
lastLap,
|
||||||
m.renderGapStr(d, fpq),
|
gapStr,
|
||||||
m.renderIntvStr(d),
|
m.renderIntvStr(d),
|
||||||
padRightVisible(trend, 8))
|
padRightVisible(trend, 8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removed lines are embedded in the previous chunk.
|
||||||
|
|
||||||
if idx == m.cursor {
|
if idx == m.cursor {
|
||||||
return styleSelected.Render(row)
|
return styleSelected.Render(row)
|
||||||
}
|
}
|
||||||
@@ -1762,13 +1749,19 @@ func (m OfficialLiveModel) renderTyreIndicator(num string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compound := strings.ToUpper(tyre.Compound)
|
compound := strings.ToUpper(tyre.Compound)
|
||||||
abbrev, style := compoundAbbrevStyle(compound)
|
abbrev, fgStyle := compoundAbbrevStyle(compound)
|
||||||
|
|
||||||
|
bgStyle := lipgloss.NewStyle().Background(fgStyle.GetForeground()).Foreground(lipgloss.Color("#000")).Bold(true)
|
||||||
|
if strings.Contains(compound, "WET") {
|
||||||
|
bgStyle = bgStyle.Foreground(lipgloss.Color("#fff"))
|
||||||
|
}
|
||||||
|
|
||||||
newMark := " "
|
newMark := " "
|
||||||
if tyre.New {
|
if tyre.New {
|
||||||
newMark = style.Render("*")
|
newMark = "*"
|
||||||
}
|
}
|
||||||
return padRightVisible(style.Render("●")+" "+abbrev+newMark, 5)
|
text := fmt.Sprintf(" %s%s ", abbrev, newMark)
|
||||||
|
return padRightVisible(bgStyle.Render(text), 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m OfficialLiveModel) renderTyreAge(num string) string {
|
func (m OfficialLiveModel) renderTyreAge(num string) string {
|
||||||
|
|||||||
@@ -600,9 +600,44 @@ function livePage() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get sortedDrivers() {
|
get sortedDrivers() {
|
||||||
return Object.values(this.drivers)
|
const merged = {};
|
||||||
.filter(d => d.Position > 0)
|
for (const num of Object.keys(this.drivers)) {
|
||||||
.sort((a, b) => a.Position - b.Position);
|
merged[num] = { ...this.drivers[num] };
|
||||||
|
}
|
||||||
|
for (const num of Object.keys(this.driverInfo)) {
|
||||||
|
if (!merged[num]) {
|
||||||
|
merged[num] = { RacingNumber: num, Position: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = Object.values(merged);
|
||||||
|
|
||||||
|
all.sort((a, b) => {
|
||||||
|
const pi = a.Position || 0;
|
||||||
|
const pj = b.Position || 0;
|
||||||
|
|
||||||
|
if (pi > 0 && pj > 0) return pi - pj;
|
||||||
|
if (pi > 0) return -1;
|
||||||
|
if (pj > 0) return 1;
|
||||||
|
|
||||||
|
const ti = a.BestLapTime || '';
|
||||||
|
const tj = b.BestLapTime || '';
|
||||||
|
if (ti && tj) return ti.localeCompare(tj);
|
||||||
|
if (ti) return -1;
|
||||||
|
if (tj) return 1;
|
||||||
|
|
||||||
|
const ni = parseInt(a.RacingNumber) || 0;
|
||||||
|
const nj = parseInt(b.RacingNumber) || 0;
|
||||||
|
return ni - nj;
|
||||||
|
});
|
||||||
|
|
||||||
|
all.forEach((d, i) => {
|
||||||
|
if (!d.Position) {
|
||||||
|
d.Position = i + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return all;
|
||||||
},
|
},
|
||||||
|
|
||||||
driverTla(num) {
|
driverTla(num) {
|
||||||
|
|||||||
@@ -142,29 +142,31 @@
|
|||||||
<div class="skeleton" style="height:36px;margin-bottom:4px;"></div>
|
<div class="skeleton" style="height:36px;margin-bottom:4px;"></div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<table class="data-table" x-show="!resultsLoading && results.length">
|
<div class="table-scroll" x-show="!resultsLoading && results.length">
|
||||||
<thead>
|
<table class="data-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>POS</th><th>DRIVER</th><th>TEAM</th><th>TIME / GAP</th><th>PTS</th>
|
<tr>
|
||||||
</tr>
|
<th>POS</th><th>DRIVER</th><th>TEAM</th><th>TIME / GAP</th><th>PTS</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template x-for="r in results" :key="r.driver_number">
|
|
||||||
<tr :style="`--team-color:#${r.team_colour}`">
|
|
||||||
<td class="pos-cell">
|
|
||||||
<span x-text="r.position||'-'"></span>
|
|
||||||
<span x-show="r.dnf" class="badge dnf">DNF</span>
|
|
||||||
<span x-show="r.dns" class="badge dns">DNS</span>
|
|
||||||
<span x-show="r.dsq" class="badge dsq">DSQ</span>
|
|
||||||
</td>
|
|
||||||
<td><span class="team-swatch"></span><span x-text="r.full_name||r.driver_number"></span></td>
|
|
||||||
<td x-text="r.team_name||'-'"></td>
|
|
||||||
<td x-text="fmtDuration(r.duration)"></td>
|
|
||||||
<td x-text="r.points||0"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
<template x-for="r in results" :key="r.driver_number">
|
||||||
|
<tr :style="`--team-color:#${r.team_colour}`">
|
||||||
|
<td class="pos-cell">
|
||||||
|
<span x-text="r.position||'-'"></span>
|
||||||
|
<span x-show="r.dnf" class="badge dnf">DNF</span>
|
||||||
|
<span x-show="r.dns" class="badge dns">DNS</span>
|
||||||
|
<span x-show="r.dsq" class="badge dsq">DSQ</span>
|
||||||
|
</td>
|
||||||
|
<td><span class="team-swatch"></span><span x-text="r.full_name||r.driver_number"></span></td>
|
||||||
|
<td x-text="r.team_name||'-'"></td>
|
||||||
|
<td x-text="fmtDuration(r.duration)"></td>
|
||||||
|
<td x-text="r.points||0"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<div x-show="!resultsLoading && !results.length" class="empty-msg">No results available.</div>
|
<div x-show="!resultsLoading && !results.length" class="empty-msg">No results available.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -243,31 +245,36 @@
|
|||||||
<span class="clock" x-text="clockDisplay"></span>
|
<span class="clock" x-text="clockDisplay"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="timing-tower">
|
<div class="table-scroll">
|
||||||
<thead>
|
<table class="timing-tower">
|
||||||
<tr><th>POS</th><th>Δ</th><th>DRIVER</th><th>TYRE</th><th>LAST LAP</th><th>GAP</th><th>BEST</th></tr>
|
<thead>
|
||||||
</thead>
|
<tr><th>POS</th><th>Δ</th><th>DRIVER</th><th>TYRE</th><th>LAST LAP</th><th>GAP</th><th>BEST</th></tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<template x-for="d in sortedDrivers" :key="d.RacingNumber">
|
<tbody>
|
||||||
<tr :class="{in-pit: d.InPit, retired: d.Retired, pit-out: d.PitOut}"
|
<template x-for="d in sortedDrivers" :key="d.RacingNumber">
|
||||||
:style="`--team-color:#${driverTeamColor(d.RacingNumber)}`">
|
<tr :class="{in-pit: d.InPit, retired: d.Retired, pit-out: d.PitOut}"
|
||||||
<td class="pos-cell" x-text="d.Position"></td>
|
:style="`--team-color:#${driverTeamColor(d.RacingNumber)}`">
|
||||||
<td class="pos-delta" x-text="posDelta(d)"></td>
|
<td class="pos-cell" x-text="d.Position"></td>
|
||||||
<td><span class="team-swatch"></span><span x-text="driverTla(d.RacingNumber)"></span>
|
<td class="pos-delta" x-text="posDelta(d)"></td>
|
||||||
<span x-show="d.InPit" class="badge pit">PIT</span>
|
<td>
|
||||||
<span x-show="d.Retired" class="badge out">OUT</span>
|
<div class="driver-cell">
|
||||||
</td>
|
<span class="team-swatch"></span><span x-text="driverTla(d.RacingNumber)"></span>
|
||||||
<td>
|
<span x-show="d.InPit" class="badge pit">PIT</span>
|
||||||
<span class="tyre-badge" :class="tyreClass(d.RacingNumber)"
|
<span x-show="d.Retired" class="badge out">OUT</span>
|
||||||
x-text="tyreLabel(d.RacingNumber)"></span>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td :class="{pb: d.LastLapPB, ob: d.LastLapOB}" x-text="d.LastLapTime||'-'"></td>
|
<td>
|
||||||
<td x-text="d.GapToLeader||'-'"></td>
|
<span class="tyre-badge" :class="tyreClass(d.RacingNumber)"
|
||||||
<td :class="{ob: d.BestLapOB}" x-text="d.BestLapTime||'-'"></td>
|
x-text="tyreLabel(d.RacingNumber)"></span>
|
||||||
</tr>
|
</td>
|
||||||
</template>
|
<td :class="{pb: d.LastLapPB, ob: d.LastLapOB}" x-text="d.LastLapTime||'-'"></td>
|
||||||
</tbody>
|
<td x-text="d.GapToLeader||'-'"></td>
|
||||||
</table>
|
<td :class="{ob: d.BestLapOB}" x-text="d.BestLapTime||'-'"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Race control messages -->
|
<!-- Race control messages -->
|
||||||
<div class="rc-panel" x-show="rcMessages.length">
|
<div class="rc-panel" x-show="rcMessages.length">
|
||||||
|
|||||||
@@ -566,8 +566,9 @@ a { color: inherit; text-decoration: none; }
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.timing-tower {
|
.timing-tower {
|
||||||
width: calc(100% - 3rem);
|
width: 100%;
|
||||||
margin: 0 1.5rem 1.5rem;
|
max-width: 1100px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -585,7 +586,7 @@ a { color: inherit; text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.timing-tower td {
|
.timing-tower td {
|
||||||
border-bottom: 1px solid var(--surface-3);
|
border-bottom: 1px solid var(--surface-3);
|
||||||
padding: 0.45rem 0.75rem;
|
padding: 0.35rem 0.6rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.timing-tower .in-pit td { background: rgba(0,51,102,0.25); }
|
.timing-tower .in-pit td { background: rgba(0,51,102,0.25); }
|
||||||
@@ -682,6 +683,7 @@ a { color: inherit; text-decoration: none; }
|
|||||||
#laps-chart,
|
#laps-chart,
|
||||||
#telemetry-chart,
|
#telemetry-chart,
|
||||||
#track-chart {
|
#track-chart {
|
||||||
|
overflow-x: auto;
|
||||||
padding: 0 1.5rem 1.5rem;
|
padding: 0 1.5rem 1.5rem;
|
||||||
}
|
}
|
||||||
#strategy-chart svg,
|
#strategy-chart svg,
|
||||||
@@ -879,3 +881,79 @@ a { color: inherit; text-decoration: none; }
|
|||||||
grid-template-columns: 44px 1fr 80px 55px 55px;
|
grid-template-columns: 44px 1fr 80px 55px 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Scrollable table wrapper ---- */
|
||||||
|
.table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
margin: 0 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
.table-scroll .data-table,
|
||||||
|
.table-scroll .timing-tower {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Mobile responsive overrides (≤ 640px)
|
||||||
|
============================================================ */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* Reduce global horizontal padding */
|
||||||
|
.home-hero { padding: 0.75rem; }
|
||||||
|
.meetings-grid { padding: 0 0.75rem 1.5rem; }
|
||||||
|
.section-label { padding: 0.75rem 0.75rem 0.5rem; }
|
||||||
|
.page-header { padding: 0.75rem 0.75rem 0.5rem; flex-wrap: wrap; gap: 0.5rem; }
|
||||||
|
.year-selector { margin-left: 0; }
|
||||||
|
.session-pills { padding: 0.5rem 0.75rem; }
|
||||||
|
.driver-selector { padding: 0.5rem 0.75rem 0.75rem; }
|
||||||
|
.table-scroll { margin: 0 0.75rem 1.5rem; }
|
||||||
|
.rc-panel { margin: 0 0.75rem 1.5rem; }
|
||||||
|
.standings-table { padding: 0 0.75rem 1.5rem; }
|
||||||
|
.skeleton-wrap { padding: 0.75rem; }
|
||||||
|
.empty-msg { padding: 2rem 0.75rem; }
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
#strategy-chart,
|
||||||
|
#laps-chart,
|
||||||
|
#telemetry-chart,
|
||||||
|
#track-chart { padding: 0 0.75rem 1.5rem; }
|
||||||
|
|
||||||
|
/* Nav */
|
||||||
|
.nav { gap: 0.75rem; padding: 0.6rem 0.75rem; }
|
||||||
|
|
||||||
|
/* Tabs — allow horizontal scroll so all tabs are reachable */
|
||||||
|
.tabs { overflow-x: auto; padding: 0 0.75rem; scrollbar-width: none; }
|
||||||
|
.tabs::-webkit-scrollbar { display: none; }
|
||||||
|
.tab { padding: 0.6rem 0.75rem; font-size: 0.78rem; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Stale banner */
|
||||||
|
.stale-banner { padding: 0.5rem 0.75rem; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* Live timing meta row */
|
||||||
|
.live-meta { gap: 0.5rem; padding: 0.5rem 0.75rem; flex-wrap: wrap; }
|
||||||
|
.clock { margin-left: 0; }
|
||||||
|
|
||||||
|
/* Tighten table cell padding */
|
||||||
|
.data-table th,
|
||||||
|
.data-table td { padding: 0.6rem 0.6rem; }
|
||||||
|
.timing-tower th,
|
||||||
|
.timing-tower td { padding: 0.4rem 0.5rem; }
|
||||||
|
|
||||||
|
/* Hide TEAM column in race results table */
|
||||||
|
.data-table th:nth-child(3),
|
||||||
|
.data-table td:nth-child(3) { display: none; }
|
||||||
|
|
||||||
|
/* Hide BEST LAP column in timing tower */
|
||||||
|
.timing-tower th:nth-child(7),
|
||||||
|
.timing-tower td:nth-child(7) { display: none; }
|
||||||
|
|
||||||
|
/* Scale down hero text */
|
||||||
|
.next-race-name { font-size: 1.35rem; }
|
||||||
|
.next-race-countdown { font-size: 1.6rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
/* Hide position delta column on very small phones to save space */
|
||||||
|
.timing-tower th:nth-child(2),
|
||||||
|
.timing-tower td:nth-child(2) { display: none; }
|
||||||
|
}
|
||||||
|
|||||||
49
suggestions.md
Normal file
49
suggestions.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Evolution Roadmap: box-box for F1 Fanatics
|
||||||
|
|
||||||
|
This document outlines the strategic evolution of **box-box** to transition from a high-quality dashboard to a "Mission Control" and "Analysis Lab" for hardcore F1 fans.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Live Session: "Mission Control"
|
||||||
|
*Goal: Enhance the viewing experience with real-time strategist-grade insights.*
|
||||||
|
|
||||||
|
### High-Impact Features
|
||||||
|
* **Command Center View**: A unified, dense dashboard combining the live timing tower, auto-detected battles, track status, weather, and race control into one glanceable screen.
|
||||||
|
* **Quali Hunter Mode**: Specifically for qualifying—tracking "banker" vs "push" laps, purple/green sector watches, traffic density alerts, and live "on the bubble" cutoff lines.
|
||||||
|
* **Strategy Watch**: Real-time undercut/overcut threat detection. Visualizing the "pit window" not just as a static number, but as a dynamic "rejoin position" gap on the timing tower.
|
||||||
|
* **Focus Mode**: Ability to "pin" 2-3 drivers to the top of the tower to monitor their specific gap trends, tyre age offsets, and lap-time deltas.
|
||||||
|
* **Chaos Meter**: A heuristic-driven indicator of "likelihood of a safety car/incident" based on bunching, weather shifts, and recent yellow sector frequency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Between GPs: "The Analysis Lab"
|
||||||
|
*Goal: Provide deep-dive tools for "nerding out" on data during the off-week.*
|
||||||
|
|
||||||
|
### High-Impact Features
|
||||||
|
* **Telemetry Lab**: Side-by-side comparison of two drivers or two laps using `CarData` (Speed, Throttle, Brake, Gear, DRS). Visualized as synchronized traces.
|
||||||
|
* **Stint & Tyre Explorer**: Detailed degradation curves. Visualizing pace drop-off over a stint and comparing "Tyre Whisperer" performance between teammates.
|
||||||
|
* **Season Storylines**: Progression charts for points, qualifying head-to-heads, and "average finish" trends across the season.
|
||||||
|
* **Driver Dossiers**: Historical performance profiles—which tracks a driver excels at, their wet-weather "rating," and comeback statistics (positions gained).
|
||||||
|
* **Track DNA**: Corner-speed profiles and overtaking difficulty maps for every circuit on the calendar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI/UX & Technical Refinements
|
||||||
|
*Goal: Professionalize the "Mission Control" aesthetic and improve ergonomics.*
|
||||||
|
|
||||||
|
* **Layout Toggles**: Switch between `Broadcast` (clean, high-level), `Dense` (standard dashboard), and `Analyst` (maximum data density).
|
||||||
|
* **Event Rail**: A horizontal timeline of the session showing Pits, Overtakes, Yellow Flags, and Team Radio icons.
|
||||||
|
* **Aggressive Pre-fetching**: Use the time *before* the 30-minute API lockout to cache all necessary historical context, track maps, and driver bios to keep the app rich during live sessions.
|
||||||
|
* **Narrative Heuristics**: Auto-generated status strings like "Verstappen is in the hunting phase" or "Hamilton tyre cliff imminent" based on lap-time trends.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Senior Engineer's "Top Pick" (Most Benefit)
|
||||||
|
|
||||||
|
If we were to prioritize only one major evolution, the **Telemetry Lab (Telemetry + Stint Comparison)** brings the most unique value to a TUI.
|
||||||
|
|
||||||
|
**Why?**
|
||||||
|
1. **Unique Value**: Most free apps show results; almost none show interactive terminal-based telemetry traces.
|
||||||
|
2. **Code Readiness**: The `CarData` and `Lap` models are already in the codebase. The API client already knows how to fetch them.
|
||||||
|
3. **Fan Engagement**: It turns the app from a "during the race" tool into a "all week long" tool. Fans love debating *where* a driver lost time (e.g., "He was 5km/h slower through Turn 4"), and a Telemetry Lab proves it.
|
||||||
|
4. **Technical Sophistication**: It demonstrates the power of the Go/Bubble Tea stack to handle dense, high-frequency data visualizations in a terminal.
|
||||||
Reference in New Issue
Block a user