- {t('clearSession.detail', { name: playerName })}
+ {t('clearSession.detail', { name: profileName })}
-
+
{t('app.clearSession')}
diff --git a/ui/src/components/panel/LivePanel.test.tsx b/ui/src/components/panel/LivePanel.test.tsx
index 56f099aeb..155ba57b5 100644
--- a/ui/src/components/panel/LivePanel.test.tsx
+++ b/ui/src/components/panel/LivePanel.test.tsx
@@ -33,7 +33,8 @@ function makeShot(overrides: Partial
= {}): Shot {
spin_source: 'measured',
spin_method: null,
carry_spin_adjusted: 214,
- player_name: 'James',
+ profile_id: 'james',
+ profile_name: 'James',
...overrides,
};
}
@@ -49,7 +50,8 @@ function render(
{}}
@@ -157,7 +159,7 @@ describe('LivePanel', () => {
expect(html).toContain('aria-pressed="true"');
});
- it('shows the player and club in the header', () => {
+ it('shows the profile and club in the header', () => {
const html = render(makeShot(), [makeShot(), makeShot(), makeShot()]);
expect(html).toContain('panel-header__subtitle">James<');
@@ -172,7 +174,8 @@ describe('LivePanel', () => {
@@ -188,9 +191,9 @@ describe('LivePanel', () => {
expect(header).toContain('panel-action');
});
- it("shows the current player's last shot, not the previous player's", () => {
+ it("shows the current profile's last shot, not the previous profile's", () => {
const james = makeShot({ ball_speed_mph: 90, timestamp: 'a' });
- const alex = makeShot({ player_name: 'Alex', ball_speed_mph: 150, timestamp: 'b' });
+ const alex = makeShot({ profile_id: 'alex', profile_name: 'Alex', ball_speed_mph: 150, timestamp: 'b' });
const html = render(alex, [james, alex]);
expect(html).toContain('>90.0<');
@@ -198,8 +201,8 @@ describe('LivePanel', () => {
expect(html).not.toContain('Ready');
});
- it('returns to ready when the current player has no shots', () => {
- const alex = makeShot({ player_name: 'Alex', ball_speed_mph: 150, timestamp: 'b' });
+ it('returns to ready when the current profile has no shots', () => {
+ const alex = makeShot({ profile_id: 'alex', profile_name: 'Alex', ball_speed_mph: 150, timestamp: 'b' });
const html = render(alex, [alex]);
expect(html).toContain('Ready');
@@ -211,14 +214,16 @@ describe('LivePanel', () => {
mode: 'swing-speed',
training_implement: 'stack-100g',
training_implement_label: 'Stack 100g',
- player_name: 'James',
+ profile_id: 'james',
+ profile_name: 'James',
});
const html = text(
renderToString(
{}}
@@ -243,7 +248,8 @@ describe('LivePanel', () => {
{
it('still warns on the ready screen so a swing is not taken without a ball', () => {
const html = text(
renderToString(
-
+
)
);
@@ -272,7 +286,8 @@ describe('LivePanel', () => {
filterShotsByPlayer(shots, playerName), [shots, playerName]);
- const displayedShot = playerShots[playerShots.length - 1] ?? null;
- const isPlayersNewShot = Boolean(isNewShot && shot && displayedShot && shot.timestamp === displayedShot.timestamp);
+ const profileShots = useMemo(() => filterShotsByProfile(shots, profileId), [shots, profileId]);
+ const displayedShot = profileShots[profileShots.length - 1] ?? null;
+ const isProfileNewShot = Boolean(isNewShot && shot && displayedShot && shot.timestamp === displayedShot.timestamp);
const swingStats = useMemo(
- () => computeSwingSpeedStats(playerShots, { playerName, trainingImplement: activeTrainingImplement }),
- [playerShots, playerName, activeTrainingImplement]
+ () => computeSwingSpeedStats(profileShots, { profileId, trainingImplement: activeTrainingImplement }),
+ [profileShots, profileId, activeTrainingImplement]
);
const metrics = useMemo(
() =>
@@ -76,7 +78,7 @@ export function LivePanel({
) : null;
- const header =
;
+ const header =
;
if (!selected) {
return (
@@ -96,7 +98,7 @@ export function LivePanel({
{header}
{ballWarning}
- {isPlayersNewShot ?
: null}
+ {isProfileNewShot ?
: null}
{metrics.map((metric) => (
{}} onShutdown={() => {}} />);
}
-describe('MenuSheet players', () => {
- it('does not manage players in the menu', () => {
+describe('MenuSheet profiles', () => {
+ it('does not manage profiles in the menu', () => {
const html = renderMenu();
- expect(html).not.toContain('menu-sheet__section-title">Player');
- expect(html).not.toContain('Add player');
+ expect(html).not.toContain('menu-sheet__section-title">Profile');
+ expect(html).not.toContain('Add profile');
expect(html).not.toContain('menu-sheet__input');
});
});
diff --git a/ui/src/components/panel/MenuSheet.tsx b/ui/src/components/panel/MenuSheet.tsx
index 787d0b200..e8838bb60 100644
--- a/ui/src/components/panel/MenuSheet.tsx
+++ b/ui/src/components/panel/MenuSheet.tsx
@@ -18,7 +18,7 @@ interface MenuSheetProps {
/**
* The sheet behind the footer logo button (design doc 6a `menuOpen6`).
*
- * 6a draws Units / Shut down. Players live on their own panel. The System
+ * 6a draws Units / Shut down. Profiles live on their own panel. The System
* block is an addition: the mockup replaced the old top header, and simulator
* and ball-detection state had nowhere else to go. Battery lives in the footer.
* Socket connection lives on the panel header LED.
diff --git a/ui/src/components/panel/PanelFooter.test.tsx b/ui/src/components/panel/PanelFooter.test.tsx
index 4db73231f..0ffcdd826 100644
--- a/ui/src/components/panel/PanelFooter.test.tsx
+++ b/ui/src/components/panel/PanelFooter.test.tsx
@@ -58,8 +58,8 @@ describe('PanelFooter', () => {
}
});
- it('hides units and battery on Players, Camera, and Debug while keeping shutdown visible', () => {
- for (const view of ['players', 'camera', 'debug'] as const) {
+ it('hides units and battery on Profiles, Camera, and Debug while keeping shutdown visible', () => {
+ for (const view of ['profiles', 'camera', 'debug'] as const) {
const html = render(view, 4, null);
expect(html).toContain('panel-footer__meta');
@@ -97,7 +97,7 @@ describe('PanelFooter', () => {
});
it('keeps the shutdown power control visible on every panel without battery telemetry', () => {
- for (const view of ['live', 'stats', 'shots', 'camera', 'players', 'debug'] as const) {
+ for (const view of ['live', 'stats', 'shots', 'camera', 'profiles', 'debug'] as const) {
const html = render(view, 0, null);
expect(html).toContain('panel-footer__power');
diff --git a/ui/src/components/panel/PanelFooter.tsx b/ui/src/components/panel/PanelFooter.tsx
index 91b90ad54..08713fe2e 100644
--- a/ui/src/components/panel/PanelFooter.tsx
+++ b/ui/src/components/panel/PanelFooter.tsx
@@ -36,7 +36,7 @@ const VIEWS_WITH_UNITS: ReadonlySet = new Set(['live', 'stats', 'shot
* Bottom bar: menu button, divider-separated panel tabs, and view meta on the
* right. Panel actions live in `PanelHeader`.
*
- * The mockup footer shows four tabs; Players and Debug are extra working
+ * The mockup footer shows four tabs; Profiles and Debug are extra working
* screens. Burying either behind a gesture makes them unreachable on the kiosk.
*/
export function PanelFooter({
diff --git a/ui/src/components/panel/PanelHeader.tsx b/ui/src/components/panel/PanelHeader.tsx
index 59bc3ca2b..0db37b97e 100644
--- a/ui/src/components/panel/PanelHeader.tsx
+++ b/ui/src/components/panel/PanelHeader.tsx
@@ -11,7 +11,7 @@ import { StatusMenu } from './StatusMenu';
interface PanelHeaderProps {
/** Uppercase panel name, e.g. "Live". */
title: string;
- /** Secondary text after the hairline divider, e.g. the player or shot count. */
+ /** Secondary text after the hairline divider, e.g. the profile or shot count. */
subtitle?: ReactNode;
/** Active club or training implement, shown after the subtitle. */
club?: ReactNode;
@@ -34,7 +34,7 @@ interface PanelHeaderProps {
}
/**
- * Hairline divider plus muted identity text. Used for the player/status
+ * Hairline divider plus muted identity text. Used for the profile/status
* subtitle and the active club so both stay visually in the same row.
*/
function IdentityPart({ children, className }: { children: ReactNode; className: string }) {
diff --git a/ui/src/components/panel/PlayersPanel.test.tsx b/ui/src/components/panel/PlayersPanel.test.tsx
deleted file mode 100644
index e3dedff46..000000000
--- a/ui/src/components/panel/PlayersPanel.test.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { renderToString } from 'react-dom/server';
-import { describe, expect, it } from 'vitest';
-import type { Shot } from '../../types/shot';
-import { PlayersPanel } from './PlayersPanel';
-
-function text(html: string): string {
- return html.replace(//g, '');
-}
-
-function makeShot(overrides: Partial = {}): Shot {
- return {
- ball_speed_mph: 90,
- club_speed_mph: 67,
- smash_factor: 1.34,
- estimated_carry_yards: 200,
- carry_range: [195, 205],
- club: 'driver',
- player_name: 'James',
- timestamp: '2026-08-19T10:00:00Z',
- peak_magnitude: 100,
- launch_angle_vertical: 13,
- launch_angle_horizontal: 0,
- launch_angle_confidence: 0.8,
- angle_source: 'radar',
- club_angle_deg: null,
- club_path_deg: null,
- spin_axis_deg: null,
- spin_rpm: 2600,
- spin_confidence: 0.9,
- spin_quality: 'high',
- spin_source: 'measured',
- spin_method: null,
- carry_spin_adjusted: null,
- ...overrides,
- };
-}
-
-function render(overrides: Partial[0]> = {}) {
- return text(
- renderToString(
- {}}
- onRemovePlayer={() => {}}
- {...overrides}
- />
- )
- );
-}
-
-describe('PlayersPanel', () => {
- it('renders a Players header with the selected name', () => {
- const html = render();
-
- expect(html).toContain('panel-header__title">Players<');
- expect(html).toContain('panel-header__subtitle">James<');
- });
-
- it('places Add player in the header', () => {
- const html = render({
- headerAction: (
-
- ),
- });
- const header = html.match(/