From 20a6ba483b8556566ae7463e82514fb89b4c07f6 Mon Sep 17 00:00:00 2001 From: jona159 Date: Wed, 17 Jun 2026 17:54:52 +0200 Subject: [PATCH 1/5] fix: respect sort order --- app/db/models/sensor.server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/db/models/sensor.server.ts b/app/db/models/sensor.server.ts index cdb1c7e7..c48c9751 100644 --- a/app/db/models/sensor.server.ts +++ b/app/db/models/sensor.server.ts @@ -94,6 +94,9 @@ export async function getSensorsWithLastMeasurement( s.title, s.unit, s.sensor_type, + s.status, + s.device_id AS "deviceId", + s."order", json_agg( json_build_object( 'value', measure.value, @@ -111,7 +114,8 @@ export async function getSensorsWithLastMeasurement( LIMIT ${count} ) AS measure ON true WHERE s.device_id = ${deviceId} - GROUP BY s.id;`, + GROUP BY s.id + ORDER BY s."order" ASC, s.id ASC;`, ) const cast = [...result].map((r) => { From 1ccb7076fff7a60c663a582584a116268c002f22 Mon Sep 17 00:00:00 2001 From: jona159 Date: Wed, 17 Jun 2026 17:55:13 +0200 Subject: [PATCH 2/5] feat: translations --- public/locales/de/device-detail-box.json | 2 ++ public/locales/en/device-detail-box.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/public/locales/de/device-detail-box.json b/public/locales/de/device-detail-box.json index 2547185e..98c1ea89 100644 --- a/public/locales/de/device-detail-box.json +++ b/public/locales/de/device-detail-box.json @@ -8,6 +8,8 @@ "open_external_link": "Externen Link öffnen", "description": "Beschreibung", "sensors": "Sensoren", + "sensor_order_hint": "Du möchtest die Reihenfolge der Sensoren ändern? Das geht in den", + "sensor_order_hint_link": "Sensoreinstellungen", "compare_devices": "Geräte vergleichen", "choose_device_for_comparison": "Wähle ein Gerät auf der Karte für den Vergleich.", "open_device_details": "Öffne Gerätedetails" diff --git a/public/locales/en/device-detail-box.json b/public/locales/en/device-detail-box.json index b758a9c0..4214e94d 100644 --- a/public/locales/en/device-detail-box.json +++ b/public/locales/en/device-detail-box.json @@ -8,6 +8,8 @@ "open_external_link": "Open external link", "description": "Description", "sensors": "Sensors", + "sensor_order_hint": "Want to change the sensor order? You can reorder them in", + "sensor_order_hint_link": "sensor settings", "compare_devices": "Compare devices", "choose_device_for_comparison": "Choose a device on the map to compare with.", "open_device_details": "Open device details" From c1d620eb831650c51d7bc3f182e1c3e4ce9f0f73 Mon Sep 17 00:00:00 2001 From: jona159 Date: Wed, 17 Jun 2026 18:01:41 +0200 Subject: [PATCH 3/5] feat: add hint how to change sensor order for device owner --- .../device-detail/device-detail-box.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index 3561d663..69035447 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -99,6 +99,11 @@ export default function DeviceDetailBox() { const sensorIds = new Set() const data = useLoaderData() + const exploreData = matches.find((match) => match.pathname === '/explore') + ?.loaderData as { user?: { id?: string } } | undefined + const isOwner = + typeof data.device?.userId === 'string' && + exploreData?.user?.id === data.device.userId const nodeRef = useRef(null) // state variables const [open, setOpen] = useState(true) @@ -111,7 +116,9 @@ export default function DeviceDetailBox() { const [sensors, setSensors] = useState() useEffect(() => { const sortedSensors = [...(data.sensors as any)].sort( - (a, b) => (a.id as unknown as number) - (b.id as unknown as number), + (a, b) => + (a.order ?? Number.MAX_SAFE_INTEGER) - + (b.order ?? Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id), ) setSensors(sortedSensors) }, [data]) @@ -192,7 +199,7 @@ export default function DeviceDetailBox() { >
+ {isOwner && ( + + + {t('sensor_order_hint')}{' '} + + {t('sensor_order_hint_link')} + + + + )}
{sensors && sensors.map( @@ -647,7 +667,7 @@ export default function DeviceDetailBox() { onClick={() => { setOpen(true) }} - className="absolute bottom-[10px] left-4 flex cursor-pointer rounded-xl border border-gray-100 bg-white shadow-lg transition-colors duration-300 ease-in-out hover:brightness-90 sm:bottom-[30px] sm:left-[10px] dark:bg-zinc-800 dark:text-zinc-200 dark:opacity-90" + className="absolute bottom-2.5 left-4 flex cursor-pointer rounded-xl border border-gray-100 bg-white shadow-lg transition-colors duration-300 ease-in-out hover:brightness-90 sm:bottom-7.5 sm:left-2.5 dark:bg-zinc-800 dark:text-zinc-200 dark:opacity-90" > From 78f1635d1b0789d730e58fa2d17ef8d188568dad Mon Sep 17 00:00:00 2001 From: jona159 Date: Thu, 18 Jun 2026 10:58:55 +0200 Subject: [PATCH 4/5] fix: order fresh devices --- app/db/models/device.server.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/db/models/device.server.ts b/app/db/models/device.server.ts index 01a148b0..b5e205f0 100644 --- a/app/db/models/device.server.ts +++ b/app/db/models/device.server.ts @@ -387,6 +387,9 @@ export async function updateDevice( ) } + let nextSensorOrder = + Math.max(...existingSensors.map((sensor) => sensor.order ?? -1)) + 1 + for (const s of args.sensors) { const hasDeleted = 'deleted' in s const hasEdited = 'edited' in s @@ -423,7 +426,9 @@ export async function updateDevice( sensorType: s.sensorType, icon: s.icon, deviceId, + order: nextSensorOrder, }) + nextSensorOrder += 1 } else if (hasEdited && s._id) { const sensorExists = existingSensors.some( (existing) => existing.id === s._id, @@ -894,7 +899,7 @@ export async function createDevice(deviceData: any, userId: string) { Array.isArray(sensorsToAdd) && sensorsToAdd.length > 0 ) { - for (const sensorData of sensorsToAdd) { + for (const [index, sensorData] of sensorsToAdd.entries()) { const [newSensor] = await tx .insert(sensor) .values({ @@ -903,6 +908,7 @@ export async function createDevice(deviceData: any, userId: string) { sensorType: sensorData.sensorType, icon: sensorData.icon, deviceId: createdDevice.id, + order: sensorData.order ?? index, }) .returning() From 0460371bd680efaa0d0d3c79f97dab6a4c3768c3 Mon Sep 17 00:00:00 2001 From: jona159 <65068389+jona159@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:12:50 +0200 Subject: [PATCH 5/5] feat: add to sensor icons, map legacy aliases (#988) * feat: add to sensor icons, map legacy aliases * fix: save sensor icon * fix: render saved sensor icon in tile view * fix: include icon and status in db call, cast sensortype to map schema property --------- Co-authored-by: Sven Heitmann --- .../device-detail/device-detail-box.tsx | 2 + app/components/sensor-icon.tsx | 8 + app/db/models/sensor.server.ts | 14 +- app/lib/sensoricons.tsx | 244 ++++++++++++------ app/routes/device.$deviceId.edit.sensors.tsx | 2 + 5 files changed, 195 insertions(+), 75 deletions(-) diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index 69035447..fa9822d6 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -523,6 +523,7 @@ export default function DeviceDetailBox() { @@ -600,6 +601,7 @@ export default function DeviceDetailBox() { diff --git a/app/components/sensor-icon.tsx b/app/components/sensor-icon.tsx index 46f5cf7a..d9e7c8af 100644 --- a/app/components/sensor-icon.tsx +++ b/app/components/sensor-icon.tsx @@ -1,11 +1,19 @@ import { Activity, ThermometerIcon, Volume1Icon } from 'lucide-react' +import { getSensorIcon } from '~/lib/sensoricons' interface SensorIconProps { title: string + icon?: string | null className: string | undefined } export default function SensorIcon(props: SensorIconProps) { + if (props.icon) { + const Icon = getSensorIcon(props.icon) + + return + } + switch (props.title.toLowerCase()) { case 'temperatur': return diff --git a/app/db/models/sensor.server.ts b/app/db/models/sensor.server.ts index c48c9751..9f9c2633 100644 --- a/app/db/models/sensor.server.ts +++ b/app/db/models/sensor.server.ts @@ -93,7 +93,8 @@ export async function getSensorsWithLastMeasurement( s.id, s.title, s.unit, - s.sensor_type, + s.sensor_type AS "sensorType", + s.icon, s.status, s.device_id AS "deviceId", s."order", @@ -171,13 +172,18 @@ export function addNewSensor({ title, unit, sensorType, + icon, deviceId, order, -}: Pick) { +}: Pick< + Sensor, + 'title' | 'unit' | 'sensorType' | 'icon' | 'deviceId' | 'order' +>) { return drizzleClient.insert(sensor).values({ title, unit, sensorType, + icon, deviceId, order, }) @@ -188,14 +194,16 @@ export function updateSensor({ title, unit, sensorType, + icon, order, -}: Pick) { +}: Pick) { return drizzleClient .update(sensor) .set({ title, unit, sensorType, + icon, order, }) .where(eq(sensor.id, id)) diff --git a/app/lib/sensoricons.tsx b/app/lib/sensoricons.tsx index 78547539..5813b56b 100644 --- a/app/lib/sensoricons.tsx +++ b/app/lib/sensoricons.tsx @@ -1,88 +1,188 @@ import { - Wifi, - ThermometerIcon, - WindIcon, + Activity, + Atom, + Battery, + CircleGauge, + Clock, + Cloud, + CloudRain, Droplets, - Tornado, - SunMoonIcon, - MicIcon, + Flame, + Gauge, + Mic, + Radiation, + Sprout, + SunMoon, + Thermometer, + Umbrella, + Volume2, + Wifi, + Wind, + Zap, + type LucideIcon, } from 'lucide-react' +const sensorIconClassName = + 'mr-1 ml-[6px] inline-block h-4 w-4 align-text-bottom text-[#818a91]' + +type SensorIconOption = { + id: string + name: LucideIcon +} + const iconsList = [ - { id: 'ThermometerIcon', name: ThermometerIcon }, - { id: 'Wifi', name: Wifi }, - { id: 'WindIcon', name: WindIcon }, + { id: 'Thermometer', name: Thermometer }, { id: 'Droplets', name: Droplets }, - { id: 'Tornado', name: Tornado }, - { id: 'SunMoonIcon', name: SunMoonIcon }, - { id: 'MicIcon', name: MicIcon }, -] + { id: 'Cloud', name: Cloud }, + { id: 'Gauge', name: Gauge }, + { id: 'CircleGauge', name: CircleGauge }, + { id: 'SunMoon', name: SunMoon }, + { id: 'Wind', name: Wind }, + { id: 'CloudRain', name: CloudRain }, + { id: 'Umbrella', name: Umbrella }, + { id: 'Volume2', name: Volume2 }, + { id: 'Mic', name: Mic }, + { id: 'Wifi', name: Wifi }, + { id: 'Battery', name: Battery }, + { id: 'Atom', name: Atom }, + { id: 'Radiation', name: Radiation }, + { id: 'Zap', name: Zap }, + { id: 'Flame', name: Flame }, + { id: 'Clock', name: Clock }, + { id: 'Sprout', name: Sprout }, + { id: 'Activity', name: Activity }, +] satisfies SensorIconOption[] + +const iconMap = Object.fromEntries( + iconsList.map((icon) => [icon.id, icon.name]), +) as Record + +const legacyIconAliases: Record = { + ThermometerIcon: 'Thermometer', + WindIcon: 'Wind', + Tornado: 'Wind', + SunMoonIcon: 'SunMoon', + MicIcon: 'Mic', + 'osem-radioactive': 'Radiation', + 'osem-particulate-matter': 'Cloud', + 'osem-moisture': 'Sprout', + 'osem-temperature-celsius': 'Thermometer', + 'osem-temperature-fahrenheit': 'Thermometer', + 'osem-drops': 'Droplets', + 'osem-thermometer': 'Thermometer', + 'osem-windspeed': 'Wind', + 'osem-sprinkles': 'CloudRain', + 'osem-brightness': 'SunMoon', + 'osem-barometer': 'Gauge', + 'osem-humidity': 'Droplets', + 'osem-not-available': 'Activity', + 'osem-gauge': 'CircleGauge', + 'osem-umbrella': 'Umbrella', + 'osem-clock': 'Clock', + 'osem-shock': 'Zap', + 'osem-fire': 'Flame', + 'osem-signal': 'Wifi', + 'osem-volume-up': 'Volume2', + 'osem-cloud': 'Cloud', + 'osem-microphone': 'Mic', + 'osem-wifi': 'Wifi', + 'osem-battery': 'Battery', + 'osem-co2': 'Atom', +} + +function normalizeIconName(iconName?: string | null) { + if (!iconName) return 'Thermometer' + + return legacyIconAliases[iconName] ?? iconName +} + +function getSensorIcon(iconName?: string | null) { + const normalizedIconName = normalizeIconName(iconName) + + return iconMap[normalizedIconName] ?? Thermometer +} function getIcon(iconName: string) { - switch (iconName) { - case 'ThermometerIcon': - return ( - - ) - case 'Wifi': - return ( - - ) - case 'WindIcon': - return ( - - ) - case 'Droplets': - return ( - - ) - case 'Tornado': - return ( - - ) - case 'SunMoonIcon': - return ( - - ) - case 'MicIcon': - return ( - - ) - } + const Icon = getSensorIcon(iconName) + + return } function assignIcon(sensorType: string, sensorTitle: string) { + const normalizedSensorType = sensorType.toLowerCase() + const normalizedSensorTitle = sensorTitle.toLowerCase() + + if ( + normalizedSensorTitle.includes('luftfeuchte') || + normalizedSensorTitle.includes('luftfeuchtigkeit') || + normalizedSensorTitle.includes('humidity') || + normalizedSensorTitle.includes('feuchte') + ) { + return + } + + if ( + normalizedSensorTitle.includes('luftdruck') || + normalizedSensorTitle.includes('pressure') || + normalizedSensorType.includes('bmp') || + normalizedSensorType.includes('dps') + ) { + return + } + + if ( + normalizedSensorTitle.includes('pm') || + normalizedSensorTitle.includes('particulate') || + normalizedSensorType.includes('pms') || + normalizedSensorType.includes('sds') || + normalizedSensorType.includes('sps') + ) { + return + } + + if ( + normalizedSensorTitle.includes('co2') || + normalizedSensorTitle.includes('co₂') || + normalizedSensorType.includes('scd') + ) { + return + } + + if ( + normalizedSensorTitle.includes('lautstärke') || + normalizedSensorTitle.includes('schalldruck') || + normalizedSensorTitle.includes('sound') || + normalizedSensorType.includes('dnms') || + normalizedSensorType.includes('sound') + ) { + return + } + + if ( + normalizedSensorTitle.includes('uv') || + normalizedSensorTitle.includes('licht') || + normalizedSensorTitle.includes('brightness') || + normalizedSensorType.includes('tsl') || + normalizedSensorType.includes('veml') + ) { + return + } + if ( - (sensorType === 'HDC1008' || sensorType === 'DHT11') && - sensorTitle === 'Temperatur' + normalizedSensorTitle.includes('wind') || + normalizedSensorType.includes('wind') ) { - return ( - - ) - } else if ( - sensorType === 'HDC1008' || - sensorTitle === 'rel. Luftfeuchte' || - sensorTitle === 'Luftfeuchtigkeit' + return + } + + if ( + normalizedSensorTitle.includes('boden') || + normalizedSensorTitle.includes('soil') ) { - return ( - - ) - } else if (sensorType === 'LM386') { - return ( - - ) - } else if (sensorType === 'BMP280' && sensorTitle === 'Luftdruck') { - return ( - - ) - } else if (sensorType === 'TSL45315' || sensorType === 'VEML6070') { - return ( - - ) - } else - return ( - - ) + return + } + + return } -export { iconsList, getIcon, assignIcon } +export { iconsList, getSensorIcon, getIcon, assignIcon } diff --git a/app/routes/device.$deviceId.edit.sensors.tsx b/app/routes/device.$deviceId.edit.sensors.tsx index 209996f6..be5a9633 100644 --- a/app/routes/device.$deviceId.edit.sensors.tsx +++ b/app/routes/device.$deviceId.edit.sensors.tsx @@ -82,6 +82,7 @@ export async function action({ request, params }: Route.ActionArgs) { title: sensor.title, unit: sensor.unit, sensorType: sensor.sensorType, + icon: sensor.icon, deviceId, order: index, }) @@ -93,6 +94,7 @@ export async function action({ request, params }: Route.ActionArgs) { title: sensor.title, unit: sensor.unit, sensorType: sensor.sensorType, + icon: sensor.icon, order: index, }) }