FIX - align health app with observable data

Remove inferred condition, recovery, and player-health values from the Health UI and NUI contract. Keep browser mocks and tests aligned, correct the default development API port, remove the misleading Medical ID disclosure, and apply the shared page gutter to prevent clipped cards.
This commit is contained in:
Type
2026-08-16 00:29:13 +02:00
parent 2f70444071
commit 6fba5bcc10
11 changed files with 11 additions and 113 deletions
-1
View File
@@ -22,7 +22,6 @@ const overview: HealthOverview = {
playerName: 'Alex Morgan',
},
previousWeekSteps: 0,
snapshot: { healthPercent: 100 },
}
describe('health store', () => {
-6
View File
@@ -368,10 +368,6 @@ const healthFallbackLocales = {
kilocalories: '{count} kcal',
thisWeek: 'This week',
snapshot: 'Health snapshot',
condition: 'Condition',
recovery: 'Recovery',
currentHealth: 'Current health',
conditions: { good: 'Good', fair: 'Fair', low: 'Low' },
trends: {
title: 'Trends',
week: 'Week',
@@ -402,8 +398,6 @@ const healthFallbackLocales = {
phoneNumber: 'Phone number',
emergencyCall: 'Emergency call',
callContact: 'Call emergency contact',
privacy:
'Information is stored with your character and can be shown to emergency services.',
saveFailed: 'Medical ID could not be saved.',
},
errors: {
-5
View File
@@ -17,17 +17,12 @@ export type HealthMedicalId = {
playerName: string
}
export type HealthSnapshot = {
healthPercent: number
}
export type HealthOverview = {
dailyStepGoal: number
days: HealthActivityDay[]
emergencyNumber: string
medicalId: HealthMedicalId
previousWeekSteps: number
snapshot: HealthSnapshot
}
export type HealthMedicalIdInput = Omit<HealthMedicalId, 'playerName'>
+1 -1
View File
@@ -34,7 +34,7 @@ describe('nuiCall', () => {
})
expect(vi.getTimerCount()).toBe(0)
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:3001/api/test',
'http://localhost:3002/api/test',
expect.objectContaining({ signal: expect.any(AbortSignal) }),
)
})
+1 -1
View File
@@ -15,7 +15,7 @@ export async function nuiCall<T = unknown>(
'apiPort',
)
const baseUrl = import.meta.env.DEV
? `http://localhost:${developmentPort ?? '3001'}/api`
? `http://localhost:${developmentPort ?? '3002'}/api`
: `https://${resourceName}`
const requestData = import.meta.env.DEV
? {
@@ -8,6 +8,7 @@ describe('HealthApp Sky UI contract', () => {
expect(source).toContain('<SkyAppPage')
expect(source).toContain('<SkyNavbar')
expect(source).toContain('<SkyScrollArea')
expect(source).toContain('padded with-tabbar')
expect(source).toContain('with-tabbar')
expect(source).toContain('<SkyPillNavigation')
expect(source).not.toContain(
@@ -26,4 +27,10 @@ describe('HealthApp Sky UI contract', () => {
expect(source).toContain("nuiCall('calls:dial'")
expect(source).toContain('health.saveMedicalId')
})
it('does not present values the phone cannot observe', () => {
expect(source).not.toContain('healthPercent')
expect(source).not.toContain('recoveryPercent')
expect(source).not.toContain('conditionKey')
})
})
+1 -76
View File
@@ -9,7 +9,6 @@ import {
MapPin,
Phone,
RefreshCw,
ShieldCheck,
Timer,
UserRound,
} from 'lucide-vue-next'
@@ -102,20 +101,6 @@ const trendCopy = computed(() => {
}
return phone.t('Apps.health.trends.same')
})
const healthPercent = computed(
() => overview.value?.snapshot.healthPercent ?? 0,
)
const conditionKey = computed(() =>
healthPercent.value >= 70
? 'good'
: healthPercent.value >= 35
? 'fair'
: 'low',
)
const recoveryPercent = computed(() => {
const activeLoad = Math.min(20, Math.round((today.value?.activeSeconds ?? 0) / 180))
return Math.max(0, Math.min(100, healthPercent.value - activeLoad))
})
const dateRange = computed(() => {
if (!visibleDays.value.length) return ''
const start = new Date(`${visibleDays.value[0].date}T12:00:00`)
@@ -248,7 +233,7 @@ onBeforeUnmount(() => {
</template>
</SkyNavbar>
<SkyScrollArea class="health-content" with-tabbar>
<SkyScrollArea class="health-content" padded with-tabbar>
<div v-if="health.isLoading && !overview" class="health-loading">
<SkySpinner />
<span>{{ phone.t('Apps.health.loading') }}</span>
@@ -333,34 +318,6 @@ onBeforeUnmount(() => {
</div>
</section>
<section class="health-section">
<h2>{{ phone.t('Apps.health.snapshot') }}</h2>
<div class="health-list">
<div class="health-list__row">
<span class="health-list__icon health-list__icon--green">
<HeartPulse :size="21" />
</span>
<span>{{ phone.t('Apps.health.condition') }}</span>
<strong :class="`health-value--${conditionKey}`">{{
phone.t(`Apps.health.conditions.${conditionKey}`)
}}</strong>
</div>
<div class="health-list__row">
<span class="health-list__icon health-list__icon--orange">
<Activity :size="21" />
</span>
<span>{{ phone.t('Apps.health.recovery') }}</span>
<strong>{{ recoveryPercent }}%</strong>
</div>
<div class="health-list__row">
<span class="health-list__icon">
<ShieldCheck :size="21" />
</span>
<span>{{ phone.t('Apps.health.currentHealth') }}</span>
<strong>{{ healthPercent }}%</strong>
</div>
</div>
</section>
</template>
<template v-else-if="activeTab === 'trends'">
@@ -568,10 +525,6 @@ onBeforeUnmount(() => {
}}
</SkyButton>
<p class="health-privacy-note">
<ShieldCheck :size="22" aria-hidden="true" />
<span>{{ phone.t('Apps.health.medicalId.privacy') }}</span>
</p>
</template>
<p v-if="actionError" class="health-action-error" role="alert">
@@ -890,15 +843,6 @@ onBeforeUnmount(() => {
color: var(--health-orange);
}
.health-list__row .health-value--good {
color: var(--health-green);
}
.health-list__row .health-value--fair {
color: var(--health-orange);
}
.health-list__row .health-value--low,
.health-list__row .health-value--accent {
color: var(--health-accent);
}
@@ -1121,25 +1065,6 @@ onBeforeUnmount(() => {
gap: 9px;
}
.health-privacy-note {
display: flex;
align-items: flex-start;
margin: 0;
padding: 14px 16px;
border: calc(var(--sky-hairline-scale) * 1px) solid var(--health-panel-border);
border-radius: 18px;
background: var(--health-panel);
color: var(--sky-muted);
font-size: 13px;
line-height: 1.35;
gap: 12px;
}
.health-privacy-note svg {
flex: none;
color: var(--health-green);
}
.health-form {
overflow: hidden;
margin: 0;
-1
View File
@@ -72,7 +72,6 @@ function healthOverview() {
emergencyNumber: '911',
medicalId: healthMedicalId,
previousWeekSteps: 36980,
snapshot: { healthPercent: 96 },
}
}
+1 -1
View File
@@ -326,7 +326,7 @@ async function verifyStatefulActions(baseUrl) {
assert.equal(health.dailyStepGoal, 8000)
assert.equal(health.days.length, 7)
assert.equal(health.days.at(-1).steps, 6420)
assert.equal(health.snapshot.healthPercent, 96)
assert.equal('snapshot' in health, false)
const medicalId = await expectSuccess(
baseUrl,
-5
View File
@@ -202,10 +202,6 @@ Locales["en"] = {
kilocalories = "{count} kcal",
thisWeek = "This week",
snapshot = "Health snapshot",
condition = "Condition",
recovery = "Recovery",
currentHealth = "Current health",
conditions = { good = "Good", fair = "Fair", low = "Low" },
trends = {
title = "Trends", week = "Week", month = "Month", total = "{count} steps",
more = "{count}% more than last week", less = "{count}% less than last week",
@@ -219,7 +215,6 @@ Locales["en"] = {
noneRecorded = "None recorded", emergencyContact = "Emergency contact",
contactName = "Contact name", relation = "Relation", phoneNumber = "Phone number",
emergencyCall = "Emergency call", callContact = "Call emergency contact",
privacy = "Information is stored with your character and can be shown to emergency services.",
saveFailed = "Medical ID could not be saved.",
},
errors = {
-16
View File
@@ -5,19 +5,6 @@ local step_progress = 0.0
local last_coords = nil
local last_report_at = GetGameTimer()
local function health_snapshot()
local ped = PlayerPedId()
if not DoesEntityExist(ped) then
return { healthPercent = 0 }
end
local health = GetEntityHealth(ped)
local maximum = math.max(1, GetEntityMaxHealth(ped))
local base = maximum > 100 and 100 or 0
local percentage = math.floor(math.max(0, math.min(100, (health - base) / (maximum - base) * 100)) + 0.5)
return { healthPercent = percentage }
end
local function flush_activity()
last_report_at = GetGameTimer()
local distance_meters = math.floor(pending_distance + 0.5)
@@ -95,9 +82,6 @@ end)
RegisterNUICallback("health:overview", function(data, cb)
local result = Bridge.Callbacks.Trigger("sky_phone:health:overview", data or {})
if result and result.success and result.data then
result.data.snapshot = health_snapshot()
end
cb(result or { success = false, error = "request_failed" })
end)