From eeafe2f0f3a3718bd52e9c333894e1abbc0611ab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 05:35:49 +0000 Subject: feat: vessel registry, crew management, and thermal alarm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vessel/CrewMember data classes with full profile fields (MMSI, callsign, flag, home port, hull type, dimensions, roles) - VesselRepository: in-memory CRUD for own vessel, fleet, and crew; getOwnVessel(), getSkipper(), getCrewByRole() helpers - VesselRegistryFragment: own-vessel card + crew list + known-vessels list; AlertDialog editors for add/edit; long-press to delete - ic_vessel.xml sailboat icon; 5th bottom-nav tab "Vessel" - ThermalMonitor: thermalFlow() using Intent.ACTION_BATTERY_CHANGED (battery °C) + PowerManager.OnThermalStatusChangedListener (API 29+); ThermalState OK/WARM/HOT/CRITICAL with distinct-until-changed filter - MainActivity: startThermalMonitoring() sounds mob_alarm + Toast on HOT (≥45°C) and CRITICAL (≥50°C); auto-silences on OK/WARM - MainViewModel: thermalState and thermalReading StateFlows - 18 VesselRepositoryTest cases — all GREEN (test-runner) - design.md: updated competitive matrix + feature implementation status https://claude.ai/code/session_011h2dXbgXg3PesQMmQUNTCW --- .../main/kotlin/org/terst/nav/vessel/CrewMember.kt | 15 ++ .../src/main/kotlin/org/terst/nav/vessel/Vessel.kt | 22 +++ .../org/terst/nav/vessel/VesselRepository.kt | 55 ++++++++ .../org/terst/nav/vessel/VesselRepositoryTest.kt | 154 +++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt create mode 100644 test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt create mode 100644 test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt create mode 100644 test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt (limited to 'test-runner/src') diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt new file mode 100644 index 0000000..0df8cb9 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt @@ -0,0 +1,15 @@ +package org.terst.nav.vessel + +data class CrewMember( + val id: String, + val name: String, + val role: CrewRole = CrewRole.CREW, + val emergencyContact: String = "", + val emergencyPhone: String = "", + val certifications: List = emptyList(), + val notes: String = "" +) + +enum class CrewRole { + SKIPPER, FIRST_MATE, WATCH_CAPTAIN, CREW, GUEST +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt new file mode 100644 index 0000000..b7c0eaf --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt @@ -0,0 +1,22 @@ +package org.terst.nav.vessel + +data class Vessel( + val id: String, + val name: String, + val mmsi: String = "", + val callsign: String = "", + val flag: String = "", + val homePort: String = "", + val hullType: HullType = HullType.MONOHULL, + val loaMeters: Double = 0.0, + val beamMeters: Double = 0.0, + val draftMeters: Double = 0.0, + val displacementKg: Double = 0.0, + val engineHp: Int = 0, + val notes: String = "", + val isOwnVessel: Boolean = false +) + +enum class HullType { + MONOHULL, CATAMARAN, TRIMARAN, POWERBOAT, OTHER +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt new file mode 100644 index 0000000..fe4c44d --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt @@ -0,0 +1,55 @@ +package org.terst.nav.vessel + +import java.util.UUID + +class VesselRepository { + + private val vessels = mutableListOf() + private val crew = mutableListOf() + + fun addVessel(vessel: Vessel): Vessel { + val v = if (vessel.id.isBlank()) vessel.copy(id = UUID.randomUUID().toString()) else vessel + vessels.removeAll { it.id == v.id } + vessels.add(v) + return v + } + + fun updateVessel(vessel: Vessel): Boolean { + val idx = vessels.indexOfFirst { it.id == vessel.id } + if (idx < 0) return false + vessels[idx] = vessel + return true + } + + fun removeVessel(id: String): Boolean = vessels.removeAll { it.id == id } + + fun getVessel(id: String): Vessel? = vessels.find { it.id == id } + + fun getVessels(): List = vessels.toList() + + fun getOwnVessel(): Vessel? = vessels.find { it.isOwnVessel } + + fun addCrewMember(member: CrewMember): CrewMember { + val m = if (member.id.isBlank()) member.copy(id = UUID.randomUUID().toString()) else member + crew.removeAll { it.id == m.id } + crew.add(m) + return m + } + + fun updateCrewMember(member: CrewMember): Boolean { + val idx = crew.indexOfFirst { it.id == member.id } + if (idx < 0) return false + crew[idx] = member + return true + } + + fun removeCrewMember(id: String): Boolean = crew.removeAll { it.id == id } + + fun getCrewMember(id: String): CrewMember? = crew.find { it.id == id } + + fun getCrew(): List = crew.toList() + + fun getCrewByRole(role: CrewRole): List = crew.filter { it.role == role } + + fun getSkipper(): CrewMember? = crew.find { it.role == CrewRole.SKIPPER } +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt b/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt new file mode 100644 index 0000000..cec2a54 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt @@ -0,0 +1,154 @@ +package org.terst.nav.vessel + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class VesselRepositoryTest { + + private lateinit var repo: VesselRepository + + @Before + fun setup() { + repo = VesselRepository() + } + + // --- Vessel tests --- + + @Test + fun `addVessel with blank id assigns a generated id`() { + val v = repo.addVessel(Vessel(id = "", name = "Perseverance")) + assertTrue(v.id.isNotBlank()) + } + + @Test + fun `addVessel with explicit id preserves id`() { + val v = repo.addVessel(Vessel(id = "abc123", name = "Perseverance")) + assertEquals("abc123", v.id) + } + + @Test + fun `getVessels returns all added vessels`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + assertEquals(2, repo.getVessels().size) + } + + @Test + fun `getVessel returns correct vessel by id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + assertEquals("Beta", repo.getVessel("2")?.name) + } + + @Test + fun `updateVessel replaces vessel data`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + val updated = repo.updateVessel(Vessel(id = "1", name = "Alpha II", loaMeters = 12.5)) + assertTrue(updated) + assertEquals("Alpha II", repo.getVessel("1")?.name) + assertEquals(12.5, repo.getVessel("1")?.loaMeters ?: 0.0, 0.001) + } + + @Test + fun `updateVessel returns false for unknown id`() { + assertFalse(repo.updateVessel(Vessel(id = "nope", name = "Ghost"))) + } + + @Test + fun `removeVessel removes by id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + repo.removeVessel("1") + assertNull(repo.getVessel("1")) + assertEquals(1, repo.getVessels().size) + } + + @Test + fun `getOwnVessel returns only own vessel`() { + repo.addVessel(Vessel(id = "1", name = "Alpha", isOwnVessel = true)) + repo.addVessel(Vessel(id = "2", name = "Beta", isOwnVessel = false)) + assertEquals("Alpha", repo.getOwnVessel()?.name) + } + + @Test + fun `getOwnVessel returns null when none set`() { + repo.addVessel(Vessel(id = "1", name = "Alpha", isOwnVessel = false)) + assertNull(repo.getOwnVessel()) + } + + @Test + fun `addVessel upserts on duplicate id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "1", name = "Alpha Updated")) + assertEquals(1, repo.getVessels().size) + assertEquals("Alpha Updated", repo.getVessel("1")?.name) + } + + // --- Crew tests --- + + @Test + fun `addCrewMember with blank id assigns generated id`() { + val m = repo.addCrewMember(CrewMember(id = "", name = "Jane Doe")) + assertTrue(m.id.isNotBlank()) + } + + @Test + fun `getCrew returns all crew members`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob")) + assertEquals(2, repo.getCrew().size) + } + + @Test + fun `getSkipper returns skipper crew member`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob", role = CrewRole.SKIPPER)) + assertEquals("Bob", repo.getSkipper()?.name) + } + + @Test + fun `getSkipper returns null when no skipper`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + assertNull(repo.getSkipper()) + } + + @Test + fun `updateCrewMember updates role and details`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + val ok = repo.updateCrewMember( + CrewMember(id = "c1", name = "Alice", role = CrewRole.FIRST_MATE, emergencyContact = "Bob") + ) + assertTrue(ok) + assertEquals(CrewRole.FIRST_MATE, repo.getCrewMember("c1")?.role) + assertEquals("Bob", repo.getCrewMember("c1")?.emergencyContact) + } + + @Test + fun `removeCrewMember removes by id`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob")) + repo.removeCrewMember("c1") + assertNull(repo.getCrewMember("c1")) + assertEquals(1, repo.getCrew().size) + } + + @Test + fun `getCrewByRole filters correctly`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob", role = CrewRole.WATCH_CAPTAIN)) + repo.addCrewMember(CrewMember(id = "c3", name = "Carol", role = CrewRole.CREW)) + val result = repo.getCrewByRole(CrewRole.CREW) + assertEquals(2, result.size) + assertTrue(result.all { it.role == CrewRole.CREW }) + } + + @Test + fun `vessel registry and crew are independent`() { + repo.addVessel(Vessel(id = "v1", name = "Alpha")) + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.removeVessel("v1") + assertEquals(1, repo.getCrew().size) + assertEquals(0, repo.getVessels().size) + } +} -- cgit v1.2.3