From 161e871c53ee39ed29240b858e1fd29ca8403682 Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Sun, 24 May 2026 20:56:00 -0400 Subject: [PATCH] UI Updates and Web App --- internal/ui/official_live.go | 55 ++++++++---------- internal/web/assets/app.js | 41 ++++++++++++- internal/web/assets/index.html | 101 ++++++++++++++++++--------------- internal/web/assets/style.css | 84 ++++++++++++++++++++++++++- suggestions.md | 49 ++++++++++++++++ 5 files changed, 246 insertions(+), 84 deletions(-) create mode 100644 suggestions.md diff --git a/internal/ui/official_live.go b/internal/ui/official_live.go index b1511fe..a770014 100644 --- a/internal/ui/official_live.go +++ b/internal/ui/official_live.go @@ -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 { diff --git a/internal/web/assets/app.js b/internal/web/assets/app.js index 453726d..adfdfd4 100644 --- a/internal/web/assets/app.js +++ b/internal/web/assets/app.js @@ -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) { diff --git a/internal/web/assets/index.html b/internal/web/assets/index.html index 531e3c3..0a5610a 100644 --- a/internal/web/assets/index.html +++ b/internal/web/assets/index.html @@ -142,29 +142,31 @@
- - - - - - - - - -
POSDRIVERTEAMTIME / GAPPTS
+ + + + + +
No results available.
@@ -243,31 +245,36 @@ - - - - - - - -
POSΔDRIVERTYRELAST LAPGAPBEST
+
+ + + + + + + +
POSΔDRIVERTYRELAST LAPGAPBEST
+
diff --git a/internal/web/assets/style.css b/internal/web/assets/style.css index 611b73f..db0a65c 100644 --- a/internal/web/assets/style.css +++ b/internal/web/assets/style.css @@ -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; } +} diff --git a/suggestions.md b/suggestions.md new file mode 100644 index 0000000..3631ea9 --- /dev/null +++ b/suggestions.md @@ -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.