Skip to content

feat: add device location height - #1068

Open
jona159 wants to merge 41 commits into
devfrom
feat/device-location-height
Open

feat: add device location height#1068
jona159 wants to merge 41 commits into
devfrom
feat/device-location-height

Conversation

@jona159

@jona159 jona159 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Dependency upgrade
  • Bug fix (non-breaking change)
  • Breaking change
    • e.g. a fixed bug or new feature that may break something else
  • New feature
  • Code quality improvements
    • e.g. refactoring, documentation, tests, tooling, ...

Implementation

Checklist

  • I gave this pull request a meaningful title
  • My pull request is targeting the dev branch
  • I have added documentation to my code
  • I have deleted code that I have commented out

Additional Information

  • This PR closes #

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 60fa7375-ce88-446c-ab12-4f6d023e1b0f

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added optional height-above-ground entry during device creation and editing.
    • Terrain elevation is retrieved automatically to calculate and display height above sea level.
    • GeoJSON and API responses now support optional three-dimensional coordinates.
    • Added elevation status, retry, attribution, and loading feedback.
  • Bug Fixes

    • Navigation validates and saves location data before changing steps.
    • Location updates preserve height information consistently.
    • Improved handling of elevation lookup and device creation errors.

Walkthrough

The change adds terrain elevation lookup and optional above-ground height handling across device forms, routes, persistence, API schemas, GeoJSON output, configuration, localization, and tests.

Changes

Device elevation support

Layer / File(s) Summary
Elevation service and location contracts
app/services/elevation-service.server.ts, app/lib/elevation.ts, app/lib/location.ts, app/lib/openapi/schemas/location.ts, app/lib/env.server.ts
The elevation service validates coordinates, queries OpenTopoData, caches results, handles failures, and calculates sea-level height from terrain elevation and above-ground height.
Creation and editing elevation UI
app/components/device/new/*, app/routes/device.$deviceId.edit.location.tsx, app/hooks/use-terrain-elevation.ts, public/locales/*
The forms collect above-ground height, request terrain elevation, calculate final height, handle loading and retry states, and display localized elevation information.
Device routes and resource API
app/routes/device.new.tsx, app/routes/api.boxes.ts, app/routes/api.boxes.$deviceId.ts, app/routes/resources.elevation.ts
Creation and update routes resolve above-ground height before persistence. The elevation resource validates authenticated requests and maps lookup failures to structured responses.
Device storage and API serialization
app/db/drizzle/*, app/db/schema/device.ts, app/db/models/*, app/lib/device-transform.ts, app/lib/openapi/schemas/*, app/services/device-service.server.ts, app/lib/api-schemas/devices.ts
Device height is stored as a nullable double-precision value, included in projections and sensor metadata, and emitted as an optional third GeoJSON coordinate.
New-device validation and navigation
app/lib/new-device-form.ts, app/components/device/new/new-device-stepper.tsx, app/components/device/new/sensors-info.tsx, app/lib/device-general.ts
Shared schemas validate the complete new-device submission. Step navigation validates and persists current-step data before moving.
Elevation and height behavior tests
tests/lib/*, tests/routes/*
Tests cover parsing, validation, persistence, zero and negative values, nullable values, and two- or three-dimensional API coordinates.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to a65e6

This change adds device-height handling across creation, editing, and API flows, but unresolved issues can silently alter or erase stored heights, persist invalid coordinates, make device creation fail or create duplicates after retries, and delay writes when elevation requests accumulate. The PR is not ready to merge until these correctness and availability risks are fixed or explicitly accepted.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant LocationForm
  participant DeviceRoute
  participant ElevationService
  participant DeviceModel
  participant Database
  Operator->>LocationForm: enter coordinates and heightAboveGround
  LocationForm->>ElevationService: request terrain elevation
  ElevationService-->>LocationForm: return terrain elevation and metadata
  LocationForm->>DeviceRoute: submit validated location
  DeviceRoute->>ElevationService: calculate height above sea level
  DeviceRoute->>DeviceModel: persist calculated height
  DeviceModel->>Database: save device height
  DeviceModel-->>DeviceRoute: return device data
  DeviceRoute-->>Operator: display 2D or 3D GeoJSON location
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description contains only an uncompleted template and provides no meaningful implementation or change summary. Complete the description with the change type, implementation details, rationale, testing information, and related issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding device location height functionality.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/device-location-height

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 65% 2290 / 3523
🔵 Statements 63.49% 2366 / 3726
🔵 Functions 62.35% 439 / 704
🔵 Branches 50.18% 1106 / 2204
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
app/db/models/device.server.ts 63.01% 67.38% 61.22% 64.09% 93-98, 106, 212, 224-225, 292-322, 342, 390, 424, 453, 485, 515, 519, 527-529, 535-537, 555-557, 561-563, 605-607, 622, 633-815, 866-870, 898-904, 909-925, 1009-1011, 1022, 1029-1030, 1042-1049, 1053-1064, 1105, 1213-1233, 1252
app/db/models/sensor.server.ts 59.18% 58.06% 47.05% 60.41% 70-72, 128-133, 158-233, 288
app/db/schema/device.ts 66.66% 100% 50% 63.63% 59-64, 79-85
app/lib/device-transform.ts 96% 93.1% 100% 100% 136
app/lib/elevation.ts 0% 0% 0% 0% 32
app/lib/location.ts 41.17% 25.58% 33.33% 42.55% 54-75, 145, 164-284
app/lib/api-schemas/devices.ts 68.29% 61.11% 68.42% 67.56% 100, 111, 120-123, 134-151, 159, 179-184
app/lib/openapi/schemas/device.ts 100% 83.33% 100% 100%
app/lib/openapi/schemas/location.ts 87.5% 50% 100% 85.71% 77-81, 106-110
app/routes/api.boxes.$deviceId.$sensorId.ts 75% 52.94% 100% 75% 155-157, 165, 181-183, 199-200, 208-209, 257-258, 262-263, 271-278
app/routes/api.boxes.$deviceId.ts 64.95% 53.33% 81.81% 66.37% 113-117, 291, 302, 316, 325-331, 337, 340, 353, 363, 369-376, 384, 388, 392-394, 401-434, 465-474, 496, 500, 506, 514, 524-529, 537, 541, 556
app/routes/api.boxes.ts 92% 87.5% 100% 92% 220, 256-264, 293
app/services/device-service.server.ts 28.33% 20.68% 16.66% 29.09% 57, 68-73, 79-187, 205, 222, 244-247
app/services/elevation-service.server.ts 13.08% 0% 0% 14.14% 52-289
Generated in workflow #2925 for commit 4cd67fa by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/db/models/device.server.ts (1)

429-469: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

updateDevice clears a stored height when the caller omits it.

Line 469 writes height ?? null whenever args.location is present. UpdateDeviceArgs.location declares height?: number, so a partial update such as { location: { lat, lng } } silently overwrites an existing height with null.

This diverges from the convention used at Lines 406-417, where the function only writes a column when the argument is not undefined. Latitude and longitude are required inside location, but height is not.

Write height only when the caller supplies it.

🛠️ Proposed fix to preserve an existing height
 			setColumns['latitude'] = lat
 			setColumns['longitude'] = lng
-			setColumns['height'] = height ?? null
+			if (height !== undefined) {
+				setColumns['height'] = height
+			}

If clearing the height must remain possible, widen the type to height?: number | null and keep the undefined guard.

🧹 Nitpick comments (2)
app/db/models/device.server.ts (1)

679-682: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the coordinate-building logic into one helper.

The same conditional now appears at Lines 679-682, at Lines 802-809, and in app/routes/api.boxes.ts at Lines 167-172. Three copies of the 2D/3D rule will drift.

Add one exported helper and reuse it in all three places.

♻️ Proposed helper
// app/lib/location.ts
export function toGeoJsonPosition(
	longitude: number,
	latitude: number,
	height: number | null | undefined,
): [number, number] | [number, number, number] {
	return height == null
		? [longitude, latitude]
		: [longitude, latitude, height]
}
 		for (const device of devices) {
-			const coordinates =
-				device.height === null
-					? [device.longitude, device.latitude]
-					: [device.longitude, device.latitude, device.height]
+			const coordinates = toGeoJsonPosition(
+				device.longitude,
+				device.latitude,
+				device.height,
+			)
 			const feature = point(coordinates, device)

Also applies to: 802-809

tests/routes/api.boxes.spec.ts (1)

355-355: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting the coordinate arity against the feature height.

expect([2, 3]).toContain(...) accepts both shapes unconditionally. A regression that drops the height from the coordinates still passes. Tie the expected length to feature.properties.height.

♻️ Proposed assertion
-				expect([2, 3]).toContain(feature.geometry.coordinates.length)
+				expect(feature.geometry.coordinates.length).toBe(
+					feature.properties.height === null ? 2 : 3,
+				)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 468a7a42-9e5c-45c9-9aa8-08641e7eee6a

📥 Commits

Reviewing files that changed from the base of the PR and between 6da7f40 and 3168052.

📒 Files selected for processing (26)
  • app/components/device/new/location-info.tsx
  • app/components/device/new/new-device-stepper.tsx
  • app/components/device/new/summary-info.tsx
  • app/db/drizzle/0048_giant_carnage.sql
  • app/db/drizzle/meta/0048_snapshot.json
  • app/db/drizzle/meta/_journal.json
  • app/db/models/device.server.ts
  • app/db/models/sensor.server.ts
  • app/db/schema/device.ts
  • app/lib/api-schemas/devices.ts
  • app/lib/device-transform.ts
  • app/lib/location.ts
  • app/lib/openapi/schemas/device.ts
  • app/lib/openapi/schemas/location.ts
  • app/routes/api.boxes.ts
  • app/routes/device.$deviceId.edit.location.tsx
  • app/routes/device.new.tsx
  • app/services/device-service.server.ts
  • public/locales/de/edit-device-general.json
  • public/locales/de/newdevice.json
  • public/locales/en/edit-device-general.json
  • public/locales/en/newdevice.json
  • tests/lib/location.spec.ts
  • tests/lib/transform-to-api-format.spec.ts
  • tests/routes/api.boxes.$deviceId.spec.ts
  • tests/routes/api.boxes.spec.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.

@jona159
jona159 marked this pull request as ready for review August 19, 2026 06:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/db/models/device.server.ts (1)

363-363: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Allow null in the update type.

updateDevice stores height: null when null is explicitly provided at Lines 470-472. The UpdateDeviceArgs.location.height type accepts only number, so typed callers cannot clear an existing height.

-	location?: { lat: number; lng: number; height?: number }
+	location?: { lat: number; lng: number; height?: number | null }
🧹 Nitpick comments (1)
app/db/models/device.server.ts (1)

470-472: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add regression tests for height preservation and clearing.

When height is omitted, the existing value must remain unchanged. When height is explicitly null, the stored value must be cleared. Add tests for both update cases.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5aacfd67-1a7d-4da8-8594-7166b09b65d6

📥 Commits

Reviewing files that changed from the base of the PR and between 3168052 and c21cdb9.

📒 Files selected for processing (3)
  • app/db/models/device.server.ts
  • app/lib/location.ts
  • tests/routes/api.boxes.spec.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 1 remains after this review.

Comment thread tests/routes/api.boxes.spec.ts
jona159 and others added 17 commits August 19, 2026 13:55
…in elevation

- Add OpenTopoData API service for fetching terrain elevation
- Uses multi-dataset query (eudem25m,srtm30m) for automatic fallback
- Update location-info.tsx to display terrain elevation and final height
- Users now input height above ground, terrain elevation is auto-fetched
- Final height above sea level is calculated and stored
- Update translations for new height labels and info text
- Update device.new.tsx action to fetch elevation and calculate final height

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/lib/api-schemas/devices.ts (1)

18-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the breaking change to location.height semantics for API clients.

CreateDeviceSchema is the public create-device request shape. Before this change, height was stored as supplied. Now the server resolves terrain elevation and adds it, per the description on DeviceHeightAboveGroundSchema in app/lib/openapi/schemas/location.ts lines 69-73.

An existing client that sends an absolute sea-level height keeps working, but the stored height shifts upward by the terrain elevation at that coordinate. The change is silent for that client.

Add a migration note to the OpenAPI description and the changelog. State the old meaning, the new meaning, and the effective date.

app/routes/device.new.tsx (1)

129-140: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Device creation and integration creation are not atomic, and the new 500 response invites duplicate devices.

Line 129 creates the device. Line 131 creates the integrations. If line 131 throws, the device already exists, but the action now returns device_creation_failed with status 500 at lines 136-139. Previously it redirected to the profile.

The client sees a total failure and the user retries. Each retry creates another device.

Either run both writes in one transaction, or return a success response that reports the integration failure separately so the user does not retry the device creation.

app/routes/api.boxes.$deviceId.ts (1)

450-477: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

A read-modify-write round trip inflates the stored height.

The PUT handler now treats body.location.height as height above ground and stores terrainElevation + height. The GET handler returns the stored height, which is above sea level. A client that reads a device, changes one unrelated field, and sends the payload back unchanged submits the sea-level height as an above-ground height. The stored height then grows by the terrain elevation on every such round trip. This is the normal read-modify-write pattern for a REST resource, so the corruption is easy to trigger.

Consider one of these options:

  • Accept an explicit field, for example heightAboveGround, and keep height as the absolute value.
  • Return the submitted above-ground height in the response so a round trip is idempotent.
  • Reject a location.height that is already resolved, using a request flag.

Also confirm how the edit UI reads the stored height. If app/routes/device.$deviceId.edit.location.tsx seeds its above-ground input from the persisted sea-level device.height, the same inflation occurs on each save.

#!/bin/bash
# Description: Trace how stored device height is read back into above-ground inputs.
fd -t f 'device.$deviceId.edit.location.tsx' app/routes --exec rg -n 'height|heightAboveGround|elevation' {}
fd -t f 'location.ts' app/lib/openapi/schemas --exec cat -n {}
rg -n 'heightAboveGround|height' app/lib/device-transform.ts app/services/device-service.server.ts
app/routes/api.boxes.ts (1)

186-189: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a nullish check for device.height.

The condition tests only null. If device.height is undefined, for example when a query selects a subset of columns, the third branch produces [longitude, latitude, undefined], which serializes to [lng, lat, null] and is not valid GeoJSON. Test for both null and undefined.

🐛 Proposed fix
 					coordinates:
-						device.height === null
+						device.height == null
 							? [device.longitude, device.latitude]
 							: [device.longitude, device.latitude, device.height],
🧹 Nitpick comments (9)
app/lib/openapi/schemas/location.ts (1)

100-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared long-form normalization transform.

Lines 100-108 repeat the transform already defined at lines 59-67 for LocationObjectSchema. Both map longitude/latitude/height to lng/lat/height.

Move the transform body into a single local helper and use it in both places.

♻️ Proposed refactor
+function normalizeLongForm(location: {
+	longitude: number
+	latitude: number
+	height?: number
+}) {
+	return { lng: location.longitude, lat: location.latitude, height: location.height }
+}

Then use normalizeLongForm(location) in both transforms.

.env.example (1)

16-19: 🩺 Stability & Availability | 🔵 Trivial

Plan for the public OpenTopoData quota.

The example points at the shared public instance api.opentopodata.org. That instance enforces a low request rate and a daily call cap. Device creation and location edits now block on this lookup, and app/routes/device.new.tsx returns HTTP 503 when the lookup fails.

For production, host a private OpenTopoData instance or add a cache for repeated coordinates. Add a metric for elevation lookup failures so quota exhaustion is visible.

app/components/device/new/summary-info.tsx (1)

20-24: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse the elevation result from the location step.

app/components/device/new/location-info.tsx line 15 already calls useTerrainElevation for the same coordinates. This call omits initialResult, so the summary step issues a second request for coordinates that were already resolved. app/routes/device.new.tsx then performs a third lookup on the server. The public OpenTopoData endpoint is rate limited.

Lift the resolved TerrainElevationResult into the stepper form state and pass it as initialResult. The hook already short-circuits when resultMatchesLocation matches.

app/routes/device.new.tsx (1)

71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Annotate terrainElevation with its type.

let terrainElevation without an annotation gets an evolving implicit any. The access terrainElevation.elevation at line 88 is therefore unchecked. app/routes/device.$deviceId.edit.location.tsx line 162 declares let terrainElevation: TerrainElevationResult.

♻️ Proposed change
-	let terrainElevation
+	let terrainElevation: TerrainElevationResult

Add the type import:

-import { calculateHeightAboveSeaLevel } from '~/lib/elevation'
+import {
+	calculateHeightAboveSeaLevel,
+	type TerrainElevationResult,
+} from '~/lib/elevation'
app/lib/location.ts (1)

101-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared field-error mapping.

parseDeviceLocationInputFormData and validateDeviceLocationInputFieldErrors build the same DeviceLocationInputFieldErrors object from a flattened error. Extract one helper and reuse it in both functions.

♻️ Proposed refactor
+function toDeviceLocationInputFieldErrors(
+	error: z.ZodError,
+): DeviceLocationInputFieldErrors {
+	const flattened = z.flattenError(error)
+
+	return {
+		latitude: flattened.fieldErrors.latitude?.[0],
+		longitude: flattened.fieldErrors.longitude?.[0],
+		heightAboveGround: flattened.fieldErrors.heightAboveGround?.[0],
+	}
+}
+
 export function parseDeviceLocationInputFormData(formData: FormData):
 	| {
 			success: true
 			data: DeviceLocationInput
 	  }
 	| {
 			success: false
 			errors: DeviceLocationInputFieldErrors
 	  } {
 	const parsed = deviceLocationInputSchema.safeParse({
 		latitude: formData.get('latitude'),
 		longitude: formData.get('longitude'),
 		heightAboveGround: formData.get('heightAboveGround'),
 	})
 
 	if (parsed.success) return { success: true, data: parsed.data }
 
-	const flattened = z.flattenError(parsed.error)
-
-	return {
-		success: false,
-		errors: {
-			latitude: flattened.fieldErrors.latitude?.[0],
-			longitude: flattened.fieldErrors.longitude?.[0],
-			heightAboveGround: flattened.fieldErrors.heightAboveGround?.[0],
-		},
-	}
+	return {
+		success: false,
+		errors: toDeviceLocationInputFieldErrors(parsed.error),
+	}
 }
 
 export function validateDeviceLocationInputFieldErrors(
 	value: unknown,
 ): DeviceLocationInputFieldErrors {
 	const parsed = deviceLocationInputSchema.safeParse(value)
 
 	if (parsed.success) return {}
 
-	const flattened = z.flattenError(parsed.error)
-
-	return {
-		latitude: flattened.fieldErrors.latitude?.[0],
-		longitude: flattened.fieldErrors.longitude?.[0],
-		heightAboveGround: flattened.fieldErrors.heightAboveGround?.[0],
-	}
+	return toDeviceLocationInputFieldErrors(parsed.error)
 }
app/components/device/new/location-info.tsx (2)

74-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract one numeric field handler.

handleLatitudeChange, handleLongitudeChange, and handleHeightChange repeat the same trim, Number, and Number.isFinite logic. Extract a single helper that parses the input and returns number | undefined, then use it in all three handlers.

♻️ Proposed refactor
+	const parseNumericInput = (rawValue: string) => {
+		const value = rawValue.trim()
+		if (value === '') return undefined
+		const parsedValue = Number(value)
+
+		return Number.isFinite(parsedValue) ? parsedValue : undefined
+	}
+
 	const handleLatitudeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-		const value = event.target.value.trim()
-		const parsedValue = Number(value)
-		const latitude =
-			value === '' || !Number.isFinite(parsedValue) ? '' : parsedValue
+		const parsed = parseNumericInput(event.target.value)
+		const latitude = parsed ?? ''
 
 		setMarker((current) => ({ ...current, latitude }))
-		setValue('latitude', latitude === '' ? undefined : latitude, {
+		setValue('latitude', parsed, {
 			shouldDirty: true,
 			shouldValidate: true,
 		})
 	}

243-291: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Announce elevation status changes and reference only existing element ids.

Two points for the elevation status block:

  • aria-describedby="height-info height-error" always references height-error, but that element renders only when errors.heightAboveGround.message exists. Build the value conditionally.
  • The loading, error, and result text replaces itself asynchronously without a live region. Screen readers do not announce the change. Add aria-live="polite" to a wrapper around the status output.
app/components/device/new/new-device-stepper.tsx (1)

147-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the error toast against repeated firing.

The effect depends on t. react-i18next returns a new t identity after a language change, so the same failed actionData shows the toast again. Track the handled actionData in a ref, or depend only on actionData.

♻️ Proposed refactor
+	const handledActionDataRef = useRef<unknown>(null)
+
 	useEffect(() => {
 		if (!actionData || actionData.ok) return
+		if (handledActionDataRef.current === actionData) return
+		handledActionDataRef.current = actionData
 
 		toast({
 			title: t('device_creation_error'),
 			description: t(actionData.error),
 			variant: 'destructive',
 		})
 	}, [actionData, t, toast])
app/routes/resources.elevation.ts (1)

42-50: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Map lookup error codes to more precise statuses.

rate_limited and timeout both return 503. Return 429 for rate_limited, with Retry-After, and 504 for timeout. Clients can then apply correct backoff instead of retrying every failure the same way.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e546959-39c1-4a61-8565-df9577de21de

📥 Commits

Reviewing files that changed from the base of the PR and between c21cdb9 and e8b1eb5.

📒 Files selected for processing (22)
  • .env.example
  • README.md
  • app/components/device/new/location-info.tsx
  • app/components/device/new/new-device-stepper.tsx
  • app/components/device/new/summary-info.tsx
  • app/hooks/use-terrain-elevation.ts
  • app/lib/api-schemas/devices.ts
  • app/lib/elevation.ts
  • app/lib/env.server.ts
  • app/lib/location.ts
  • app/lib/openapi/schemas/location.ts
  • app/routes/api.boxes.$deviceId.ts
  • app/routes/api.boxes.ts
  • app/routes/device.$deviceId.edit.location.tsx
  • app/routes/device.new.tsx
  • app/routes/resources.elevation.ts
  • app/services/elevation-service.server.ts
  • public/locales/de/edit-device-general.json
  • public/locales/de/newdevice.json
  • public/locales/en/edit-device-general.json
  • public/locales/en/newdevice.json
  • tests/lib/location.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • public/locales/de/edit-device-general.json
  • public/locales/en/edit-device-general.json
  • public/locales/de/newdevice.json

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread app/components/device/new/summary-info.tsx Outdated
Comment thread app/components/device/new/summary-info.tsx
Comment thread app/hooks/use-terrain-elevation.ts
Comment thread app/lib/openapi/schemas/location.ts
Comment thread app/routes/device.$deviceId.edit.location.tsx
Comment thread app/routes/device.$deviceId.edit.location.tsx
Comment thread app/routes/device.new.tsx
Comment thread app/routes/device.new.tsx Outdated
Comment thread app/services/elevation-service.server.ts
Comment thread public/locales/en/newdevice.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/device.new.tsx (1)

74-94: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make terrain elevation lookup conditional on heightAboveGround.

When heightAboveGround is omitted, this action still persists the terrain elevation because calculateHeightAboveSeaLevel treats the value as 0. This creates a non-null height and makes UI device creation depend on the elevation service. Initialize finalHeight to null and calculate it only when heightAboveGround !== undefined, matching app/routes/api.boxes.ts.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62f881cd-c158-41b3-8427-d8d3806a5b64

📥 Commits

Reviewing files that changed from the base of the PR and between e8b1eb5 and ffde7d3.

📒 Files selected for processing (12)
  • app/components/device/new/new-device-stepper.tsx
  • app/components/device/new/sensors-info.tsx
  • app/components/device/new/summary-info.tsx
  • app/db/models/device.server.ts
  • app/hooks/use-terrain-elevation.ts
  • app/lib/device-general.ts
  • app/lib/new-device-form.ts
  • app/routes/device.$deviceId.edit.location.tsx
  • app/routes/device.new.tsx
  • app/services/elevation-service.server.ts
  • public/locales/de/newdevice.json
  • public/locales/en/newdevice.json
💤 Files with no reviewable changes (2)
  • public/locales/en/newdevice.json
  • public/locales/de/newdevice.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/routes/device.$deviceId.edit.location.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread app/services/elevation-service.server.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/routes/api.boxes.$deviceId.spec.ts (1)

19-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert elevation-service call arguments.

Both mocks ignore latitude and longitude. A reversed argument order would still return 250 + height and pass the current assertions. Use hoisted vi.fn mocks and assert (latitude, longitude, height) for the existing three-dimensional and zero-height POST and PUT cases in both files.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 033ea010-9d4a-4a95-8533-bfd3e61986be

📥 Commits

Reviewing files that changed from the base of the PR and between ffde7d3 and a65e6d7.

📒 Files selected for processing (2)
  • tests/routes/api.boxes.$deviceId.spec.ts
  • tests/routes/api.boxes.spec.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@jona159 jona159 linked an issue Aug 21, 2026 that may be closed by this pull request
@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

When creating a new device, I select a location and then enter a height (e.g. 10 m) and the computed height is updated accordingly.

Note: I played with some reeeeelly large numbers (e.g. 10000000000000000000000000000) in the height field beforehand.

Screenshot 2026-08-25 at 10 38 14

When I then move the location to another place without adjusting the height, the computed height is not updated (and stays 25 m in my example):

Screenshot 2026-08-25 at 10 38 50

Bonus finding:

When setting the location into the middle of the ocean, the heights are not only not updated properly but Gschätzte Oberflächenhöhe does not match at all anymore:

Screenshot 2026-08-25 at 10 38 35

@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

I added a height of 5 meter to an existing box without height and when I open the settings of that device, I see 5,001. What is happening here?

Screenshot 2026-08-25 at 10 51 46

@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

Wouldn't it also be interesting to display the height above sea level here as well?

Screenshot 2026-08-25 at 10 58 20

@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

With your current implementation, some tests seem to fail.

Comment thread app/lib/openapi/schemas/device.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea to abort the whole process when we are not able to derive a height above zero based on the input height?

Comment thread app/routes/api.boxes.ts
const sensorsProvided = validatedData.sensors?.length > 0
// Extract longitude and latitude from location array [longitude, latitude]
const [longitude, latitude] = validatedData.location
// Request height is relative to ground; persisted height is above sea level.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you once mentioned that we persist the raw height (above ground) and compute the height above sea level were needed. Here, it looks like we store the absolute height above sea level. Intended?

export default function EditLocation() {
const { device } = useLoaderData<typeof loader>()
const { t } = useTranslation('edit-device-general')
const initialHeightAboveGround = device.height === null ? null : undefined

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we simply load the height of the device (not above the sea level). I'm slightly confused what we actually store for each device.


const serverErrors: LocationFieldErrors =
const derivedHeight = Number(
(device.height - originalElevation.result.elevation).toFixed(3),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh! We store the height above sea level and, where needed, compute the height above ground again on the fly?

@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

Looks good to me, except one detail:

If the elevation api can not be called, I can't update the location of an existing device and I am also not able to create a new device!

Screenshot 2026-08-25 at 17 33 05

In this case, we should simply not allow adding a height.

@zven

zven commented Aug 25, 2026

Copy link
Copy Markdown
Member

Another consideration: Since we are sending locations to an external service, we might need to include that detail in the privacy policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add device location height

2 participants