From 0286e0783854e09e1b8b14bb5d064d303e43b96a Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 10 Mar 2026 16:01:20 +0100 Subject: [PATCH 1/3] refactor: style device overview page --- app/routes/device.$deviceId.overview.tsx | 184 +++++++++++++++-------- public/locales/de/device-overview.json | 18 ++- public/locales/en/device-overview.json | 18 ++- 3 files changed, 145 insertions(+), 75 deletions(-) diff --git a/app/routes/device.$deviceId.overview.tsx b/app/routes/device.$deviceId.overview.tsx index 5bbbbf313..eae74f027 100644 --- a/app/routes/device.$deviceId.overview.tsx +++ b/app/routes/device.$deviceId.overview.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft, ClipboardCopy, CopyCheck } from 'lucide-react' import { useTranslation } from 'react-i18next' import { redirect, @@ -13,8 +13,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card' import { getDeviceWithoutSensors } from '~/models/device.server' import { getSensorsFromDevice } from '~/models/sensor.server' import { getUserId } from '~/utils/session.server' +import { useEffect, useState } from 'react' -//***************************************************** export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await getUserId(request) if (!userId) return redirect('/') @@ -25,109 +25,155 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const deviceData = await getDeviceWithoutSensors({ id: params.deviceId }) const sensorsData = await getSensorsFromDevice(params.deviceId) - return { deviceData, sensorsData, userId } -} + // If the user is accessing someone elses device, the apiKey should not be leaked + if (deviceData && userId !== deviceData.userId) deviceData.apiKey = null -//***************************************************** -export async function action() { - return {} + return { deviceData, sensorsData, userId } } -//********************************** export default function DeviceOverview() { const { deviceData, sensorsData, userId } = useLoaderData() - const {t} = useTranslation('device-overview') + const { t } = useTranslation('device-overview') + const [copiedToClipboard, setCopiedToClipboard] = useState( + null, + ) + + const copyToClipboard = async (id, value) => { + await navigator.clipboard.writeText(value) + setCopiedToClipboard(id) + } + + useEffect(() => { + if (copiedToClipboard === null) return + const timer = window.setTimeout(() => { + setCopiedToClipboard(null) + }, 2_500) + + return () => window.clearTimeout(timer) + }, [copiedToClipboard]) return ( -
+
-
- - -
+

+ + {t('back_to_dashboard')} +

+ +
-

{t('device_overview')}

-

- {t('show_details')} -

+

+ {t('device_overview')} +

+

{t('show_details')}

- {/* sensebox table */} - + {t('device')} - + - - Name + + {t('name_label')} - - {deviceData?.name} + +
+
{deviceData?.name}
+
+ {copiedToClipboard === 'name' ? ( + + ) : ( + + copyToClipboard('name', deviceData?.name) + } + /> + )} +
+
- - Model + + {t('model_label')} - + {deviceData?.model} - - Tag + + {t('tags_label')} - + {deviceData?.tags} - {t('exposure')} - + + {t('exposure')} + + {deviceData?.exposure} - ID - - {deviceData?.id} + ID + +
+
{deviceData?.id}
+
+ {copiedToClipboard === 'id' ? ( + + ) : ( + + copyToClipboard('id', deviceData?.id) + } + /> + )} +
+
- {userId === deviceData?.userId && ( - - - API Key - - - {deviceData?.apiKey} - - + {deviceData?.apiKey && ( + + + {t('api_key_label')} + + +
+
{deviceData?.apiKey}
+
+ {copiedToClipboard === 'apiKey' ? ( + + ) : ( + + copyToClipboard('apiKey', deviceData?.apiKey) + } + /> + )} +
+
+
+
)}
- {/* sensers table */} - + {t('sensors')} @@ -136,11 +182,28 @@ export default function DeviceOverview() { {sensorsData.map((sensor) => ( - + {sensor?.title} - - {sensor?.id} + +
+
{sensor?.id}
+
+ {copiedToClipboard === + `${sensor?.title}_${sensor?.id}` ? ( + + ) : ( + + copyToClipboard( + `${sensor?.title}_${sensor?.id}`, + sensor?.id, + ) + } + /> + )} +
+
))} @@ -148,8 +211,7 @@ export default function DeviceOverview() {
-
-
+
) } diff --git a/public/locales/de/device-overview.json b/public/locales/de/device-overview.json index 45dc63501..01088bbb4 100644 --- a/public/locales/de/device-overview.json +++ b/public/locales/de/device-overview.json @@ -1,8 +1,12 @@ { - "back_to_dashboard": "Zurück zum Dashboard", - "device_overview": "Übersicht des Geräts", - "show_details": "Gerätedetails und Sensoren", - "device": "Gerät", - "exposure": "Standort", - "sensors": "Sensoren" -} \ No newline at end of file + "back_to_dashboard": "Zurück zum Dashboard", + "device_overview": "Übersicht des Geräts", + "show_details": "Gerätedetails und Sensoren", + "device": "Gerät", + "exposure": "Standort", + "sensors": "Sensoren", + "name_label": "Name", + "model_label": "Modell", + "tags_label": "Tags", + "api_key_label": "API Key" +} diff --git a/public/locales/en/device-overview.json b/public/locales/en/device-overview.json index 7af00b8db..71c661d3f 100644 --- a/public/locales/en/device-overview.json +++ b/public/locales/en/device-overview.json @@ -1,8 +1,12 @@ { - "back_to_dashboard": "Back to Dashboard", - "device_overview": "Device overview", - "show_details": "Device details and sensors.", - "device": "Device", - "exposure": "Exposure", - "sensors": "Sensors" -} \ No newline at end of file + "back_to_dashboard": "Back to Dashboard", + "device_overview": "Device overview", + "show_details": "Device details and sensors.", + "device": "Device", + "exposure": "Exposure", + "sensors": "Sensors", + "name_label": "Name", + "model_label": "Model", + "tags_label": "Tags", + "api_key_label": "API Key" +} From 85d43977f8b86aeff280e33ec2aed7ae77b82dc1 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 10 Mar 2026 16:05:25 +0100 Subject: [PATCH 2/3] feat: show api key field even if empty for owning users --- app/routes/device.$deviceId.overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/device.$deviceId.overview.tsx b/app/routes/device.$deviceId.overview.tsx index eae74f027..8e1fa6735 100644 --- a/app/routes/device.$deviceId.overview.tsx +++ b/app/routes/device.$deviceId.overview.tsx @@ -145,7 +145,7 @@ export default function DeviceOverview() { - {deviceData?.apiKey && ( + {userId === deviceData?.userId && ( {t('api_key_label')} From 06ec715fd712cb979499b56fcb918636e4300728 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 10 Mar 2026 16:21:03 +0100 Subject: [PATCH 3/3] feat: add resilience checks --- app/routes/device.$deviceId.overview.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/routes/device.$deviceId.overview.tsx b/app/routes/device.$deviceId.overview.tsx index 8e1fa6735..ec4d40b53 100644 --- a/app/routes/device.$deviceId.overview.tsx +++ b/app/routes/device.$deviceId.overview.tsx @@ -1,4 +1,5 @@ import { ArrowLeft, ClipboardCopy, CopyCheck } from 'lucide-react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { redirect, @@ -13,7 +14,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card' import { getDeviceWithoutSensors } from '~/models/device.server' import { getSensorsFromDevice } from '~/models/sensor.server' import { getUserId } from '~/utils/session.server' -import { useEffect, useState } from 'react' export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await getUserId(request) @@ -38,7 +38,11 @@ export default function DeviceOverview() { null, ) - const copyToClipboard = async (id, value) => { + const copyToClipboard = async ( + id: string, + value: string | undefined | null, + ) => { + if (value === undefined || value === null) return await navigator.clipboard.writeText(value) setCopiedToClipboard(id) }