feat(clubs): pick the club shots are filed under - #25
Merged
Merged
Conversation
Adds a club picker to the Live screen, wired to the server's set_club / club_changed events and to the club session_state restores on connect. The picker shows the club the server reports, not the last tap: the server ignores an unknown club without replying, so only its club_changed confirmation updates the store. set_club is sent only over a live connection, because Socket.IO buffers emits during a transient drop and would replay a stale pick on reconnect. The picker is disabled unless connected, and the club is cleared on a deliberate disconnect or a switch to a different server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
btripp
added a commit
to btripp/openflight-mobile
that referenced
this pull request
Sep 22, 2026
Club selection (open-flight#25) and the profile roster touch the same places in services/socket.ts. Both are kept: switching servers or disconnecting deliberately now clears the roster and the club, and the profile mutations use main's emitWhileConnected helper instead of this branch's identical emitProfileMutation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
btripp
added a commit
to btripp/openflight-mobile
that referenced
this pull request
Sep 22, 2026
Resolves conflicts with club selection (open-flight#25) in services/socket.ts and its tests by keeping both features. The two copies of emitWhileConnected are folded into one helper with an optional payload, as both PRs anticipated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
johnpacino
pushed a commit
that referenced
this pull request
Sep 23, 2026
…tdown (#26) * feat(device): add a Device tab with status, controls and graceful shutdown The Device tab was a placeholder, so a headless Pi could not be inspected or stopped from the phone at all. Stopping one meant pulling power on a live SD card, which is how Pis get corrupted. Everything here consumes contracts the server already exposes: - Status: `trigger_status` (mode, radar link, port, trigger type, accept and reject counters) and `power_status` (state, charge, voltage, provider). Requested on every (re)connect, since a phone joining a session already in progress cannot rely on having seen the server's unprompted push. - Controls: `toggle_debug`, `toggle_camera` and `toggle_camera_stream`, each sent only over a live connection. Socket.IO buffers emits through a transient drop and replays them on reconnect, which would otherwise flip recording or the camera behind the user's back. - Graceful shutdown: `POST /api/shutdown` behind a two-step confirm with observable pending, success, error and retry states. Deliberately excluded: `set_radar_config`. The server refuses radar config in mock mode, so it cannot be honestly verified without hardware. Notes for review: - `radar_connected` is `monitor is not None and not mock_mode`, so it reads false in mock mode while everything works. The UI reports the mode instead of calling a working setup "offline". - A nullable measurement renders an em dash, never 0, matching the Shots screen. A mains-powered Pi reports `available: false` and is shown as "No battery" rather than an empty one. - The shutdown outcome deliberately outlives the connection: a successful shutdown drops the socket ~0.5s after the server answers, and the "wait for its lights to settle" warning has to survive that drop to be read at all. - The debug card waits for the server before offering a control. Debug mode is server-global, so a default "Start" could have stopped a capture that was already running. - `emitWhileConnected` duplicates an equivalent guard on #21 and #25; whoever merges last should fold the three into one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMGAzMMWRuxRht8aGciAv * fix(device): drop camera controls the server does not implement The Camera card requested `get_camera_status` and emitted `toggle_camera` and `toggle_camera_stream`, but the current server has none of those handlers: its camera surface is `get_camera_capture_settings` / `camera_capture_settings`. On today's server the card could never receive a status, so it is removed along with its type, store fields and tests rather than left as dead UI. Also corrects two comments: debug status is not pushed on connect (only power and trigger status are), and the debug payload comments no longer cite server line numbers that have drifted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(device): stop OpenFlight, not "the Pi", at the connected server `POST /api/shutdown` cleans up the OpenFlight server and exits its process (_shutdown_process_after_delay in server.py); it does not halt the operating system. The screen called this "Shut down the Pi" and told the user to wait for its lights to settle before cutting power, which invites the power pull on a live SD card this feature was meant to prevent. The flow is now labelled "Stop OpenFlight", says the Pi itself stays on, and gives no power advice. The request was also addressed to the URL reloaded from storage at confirmation time. Saving the connected URL is asynchronous and allowed to fail, so after switching from Pi A to Pi B the stored value could still be A. The screen now uses the socket's active address (`socketService.currentUrl()`), captured when the user confirms. A retry reuses that captured address: while switching servers the failure card stays up until the new server connects, and re-reading the current address there would stop a Pi nobody confirmed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why was this required?
Every shot the server records is filed under a club (
clubon each shot), but the phone had no way to see or change which one. The only way to switch clubs was walking back to the kiosk between shots. On a headless Pi there was no way at all, and every shot was filed under whatever club was last set. That breaks the per-club numbers the kiosk and the phone both show.The server already owns the whole surface:
set_club, aclub_changedbroadcast to every client, and the current club insession_state. Mobile already had the club list mirrored (#19) andClubChangedPayloadtyped. Nothing was wired to them.What does this PR do?
Adds a Club row to the Live screen. Tapping it opens a sheet listing every club, grouped as Irons / Hybrids / Woods like the kiosk's picker, with the current one highlighted. Picking one sends
set_club.club_changed(or theclubinsession_stateon connect). There's no optimistic update to roll back.Not included: the roadmap's "prompt for a club on first connect", swing-speed training implements (
set_training_implement), and profile selection. The profile picker follows once #21 lands.Automated tests
__tests__/socket.test.ts, 11 added: club seeded fromsession_state; kept when a snapshot has noclub(older servers);club_changedapplied; malformedclub_changedignored (missing, non-string, null);set_clubpayload while connected; store unchanged until the server confirms; nothing sent before connecting; nothing sent during a transient drop, even after reconnecting; club kept through a drop; cleared on deliberatedisconnect(); cleared when connecting to a different server. The socket fake now tracksconnected, the same way feat(profiles): mirror the server's profile roster #21's does.__tests__/ClubPicker.test.tsx, 10 added (RNTL interactions): shows the server's club / "not set"; opening lists all three groups with the current club markedselected; picking callssetCluband closes; the row keeps the old club until confirmation; Done closes without sending; disabled whiledisconnected,connectinganderror; the sheet closes if the connection drops while it's open.__tests__/LiveScreen.test.tsx: the Live screen renders the picker once connected.I checked that the component tests catch real breakage: removing the
disabledguard and the close-on-drop guard makes 4 tests fail.All run on the head commit:
npm ci,npx expo-doctor(21/21),npx tsc --noEmit,npm test -- --ci --runInBand(14 suites / 171 tests),npm run format:check,npx expo lint,npx expo export --platform all.Manual (human) testing
Platform: iOS 26.5 simulator, iPhone 17 Pro. Build: Expo Go 57.0.9, not a development or release build. Server: the OpenFlight Python server in
--mockmode from open-flight/openflightmain.club_changedbroadcast for every one, in order.club_changed {"club": "pw"}.session_stateon connect;club_changedbroadcasts after the restart).The blue gear is Expo Go's developer button, not part of the app. In Expo Go it overlaps the sheet's Done button; it isn't there in a real build.
Not tested: Android (no emulator run for this branch), a development or release build, a physical device, and swing-speed mode.
Server/API contract impact
No server change. Consumes the existing contract:
set_club {club}, whereclubis one of theClubTypevalues mirrored indata/clubs.ts.club_changed {club}, and theclubfield ofsession_state.Adds optional
club?: stringtoSessionStatePayload. It's optional because older servers don't send it; when it's absent the current club is kept, not cleared.Overlap with #21: both PRs add the same "emit only while connected" guard (
emitWhileConnectedhere,emitProfileMutationthere). Whichever merges second should switch to the other's helper so there's one copy. I'll do that when rebasing.AI assistance
AI assistance (Claude Code) wrote the store and socket changes, the picker component and the tests, and ran the checks and simulator verification above. The design choices (confirmed-only display, the connected-only guard, clearing on a server switch) follow the review feedback on #21 and the kiosk's own
set_club/club_changedhandling. I tested the picker by hand on the simulator.Checklist
npm cisucceedsnpx expo-doctorpassesnpx tsc --noEmitpassesnpm test -- --ci --runInBandpassesnpx expo export --platform allsucceeds when application code changed