UI Updates and Web App

This commit is contained in:
2026-05-24 20:56:00 -04:00
parent b61f9f0685
commit 161e871c53
5 changed files with 246 additions and 84 deletions

View File

@@ -1616,34 +1616,14 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
tlaStr := tlaStyle.Render(padRight(tla, 4))
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
lastLapRaw := d.LastLapTime
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 {
lastLap = padRightVisible(stylePurple.Render(lastLapRaw), 10)
} else if d.LastLapPB {
@@ -1652,6 +1632,11 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
}
var row string
gapStr := m.renderGapStr(d, fpq)
if d.Retired {
gapStr = padRightVisible(styleMuted.Render("Retired"), 10)
}
if m.showSectors {
timeCol := lastLap
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[1]),
m.renderSector(d.Sectors[2]),
m.renderGapStr(d, fpq))
gapStr)
} else if fpq {
// FP / Qualifying: show BEST lap as primary, LAST as secondary
bestLap := m.renderBestLapTime(d, overallBest)
@@ -1677,7 +1662,7 @@ func (m OfficialLiveModel) renderDriverRow(d LiveDriverData, idx int, fpq bool,
flyingIndicator,
bestLap,
lastLap,
m.renderGapStr(d, fpq))
gapStr)
// Highlight danger zone (cutoff) in qualifying
if d.Cutoff && idx != m.cursor {
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,
m.renderTyreAge(d.RacingNumber),
lastLap,
m.renderGapStr(d, fpq),
gapStr,
m.renderIntvStr(d),
padRightVisible(trend, 8))
}
// Removed lines are embedded in the previous chunk.
if idx == m.cursor {
return styleSelected.Render(row)
}
@@ -1762,13 +1749,19 @@ func (m OfficialLiveModel) renderTyreIndicator(num string) string {
}
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 := " "
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 {

View File

@@ -600,9 +600,44 @@ function livePage() {
},
get sortedDrivers() {
return Object.values(this.drivers)
.filter(d => d.Position > 0)
.sort((a, b) => a.Position - b.Position);
const merged = {};
for (const num of Object.keys(this.drivers)) {
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) {

View File

@@ -142,7 +142,8 @@
<div class="skeleton" style="height:36px;margin-bottom:4px;"></div>
</template>
</div>
<table class="data-table" x-show="!resultsLoading && results.length">
<div class="table-scroll" x-show="!resultsLoading && results.length">
<table class="data-table">
<thead>
<tr>
<th>POS</th><th>DRIVER</th><th>TEAM</th><th>TIME / GAP</th><th>PTS</th>
@@ -165,6 +166,7 @@
</template>
</tbody>
</table>
</div>
<div x-show="!resultsLoading && !results.length" class="empty-msg">No results available.</div>
</div>
@@ -243,6 +245,7 @@
<span class="clock" x-text="clockDisplay"></span>
</div>
<div class="table-scroll">
<table class="timing-tower">
<thead>
<tr><th>POS</th><th>Δ</th><th>DRIVER</th><th>TYRE</th><th>LAST LAP</th><th>GAP</th><th>BEST</th></tr>
@@ -253,9 +256,12 @@
:style="`--team-color:#${driverTeamColor(d.RacingNumber)}`">
<td class="pos-cell" x-text="d.Position"></td>
<td class="pos-delta" x-text="posDelta(d)"></td>
<td><span class="team-swatch"></span><span x-text="driverTla(d.RacingNumber)"></span>
<td>
<div class="driver-cell">
<span class="team-swatch"></span><span x-text="driverTla(d.RacingNumber)"></span>
<span x-show="d.InPit" class="badge pit">PIT</span>
<span x-show="d.Retired" class="badge out">OUT</span>
</div>
</td>
<td>
<span class="tyre-badge" :class="tyreClass(d.RacingNumber)"
@@ -268,6 +274,7 @@
</template>
</tbody>
</table>
</div>
<!-- Race control messages -->
<div class="rc-panel" x-show="rcMessages.length">

View File

@@ -566,8 +566,9 @@ a { color: inherit; text-decoration: none; }
font-size: 0.85rem;
}
.timing-tower {
width: calc(100% - 3rem);
margin: 0 1.5rem 1.5rem;
width: 100%;
max-width: 1100px;
margin: 0 auto 1.5rem;
border-collapse: collapse;
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
@@ -585,7 +586,7 @@ a { color: inherit; text-decoration: none; }
}
.timing-tower td {
border-bottom: 1px solid var(--surface-3);
padding: 0.45rem 0.75rem;
padding: 0.35rem 0.6rem;
vertical-align: middle;
}
.timing-tower .in-pit td { background: rgba(0,51,102,0.25); }
@@ -682,6 +683,7 @@ a { color: inherit; text-decoration: none; }
#laps-chart,
#telemetry-chart,
#track-chart {
overflow-x: auto;
padding: 0 1.5rem 1.5rem;
}
#strategy-chart svg,
@@ -879,3 +881,79 @@ a { color: inherit; text-decoration: none; }
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
View 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.