diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index 7b810263..4e806c91 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -1,675 +1,675 @@ -import clsx from "clsx"; -import { format, formatDistanceToNow } from "date-fns"; +import clsx from 'clsx' +import { format, formatDistanceToNow } from 'date-fns' import { - ChevronUp, - Minus, - Share2, - XSquare, - EllipsisVertical, - X, - ExternalLink, - Scale, - Archive, - Cpu, - Rss, - CalendarPlus, - Hash, - LandPlot, - Image as ImageIcon, -} from "lucide-react"; -import { Fragment, useEffect, useRef, useState } from "react"; -import { isTablet, isBrowser } from "react-device-detect"; -import Draggable, { type DraggableData } from "react-draggable"; + ChevronUp, + Minus, + Share2, + XSquare, + EllipsisVertical, + X, + ExternalLink, + Scale, + Archive, + Cpu, + Rss, + CalendarPlus, + Hash, + LandPlot, + Image as ImageIcon, +} from 'lucide-react' +import { Fragment, useEffect, useRef, useState } from 'react' +import { isTablet, isBrowser } from 'react-device-detect' +import Draggable, { type DraggableData } from 'react-draggable' import { - useLoaderData, - useMatches, - useNavigate, - useNavigation, - useParams, - useSearchParams, - Link, -} from "react-router"; -import SensorIcon from "../sensor-icon"; -import Spinner from "../spinner"; + useLoaderData, + useMatches, + useNavigate, + useNavigation, + useParams, + useSearchParams, + Link, +} from 'react-router' +import SensorIcon from '../sensor-icon' +import Spinner from '../spinner' import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '../ui/accordion' +import { Alert, AlertDescription, AlertTitle } from '../ui/alert' import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "../ui/alert-dialog"; -import { Badge } from "../ui/badge"; -import { Button } from "../ui/button"; + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '../ui/alert-dialog' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "../ui/card"; + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '../ui/card' import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { Separator } from "../ui/separator"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '../ui/dropdown-menu' +import { Separator } from '../ui/separator' import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; -import { useToast } from "../ui/use-toast"; -import EntryLogs from "./entry-logs"; -import ShareLink from "./share-link"; -import { useGlobalCompareMode } from "./useGlobalCompareMode"; -import { type loader } from "~/routes/explore.$deviceId"; -import { type SensorWithLatestMeasurement } from "~/schema"; -import { getArchiveLink } from "~/utils/device"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../ui/tooltip' +import { useToast } from '../ui/use-toast' +import EntryLogs from './entry-logs' +import ShareLink from './share-link' +import { useGlobalCompareMode } from './useGlobalCompareMode' +import { type loader } from '~/routes/explore.$deviceId' +import { type SensorWithLatestMeasurement } from '~/schema' +import { getArchiveLink } from '~/utils/device' export interface MeasurementProps { - sensorId: string; - time: Date; - value: string; - min_value: string; - max_value: string; + sensorId: string + time: Date + value: string + min_value: string + max_value: string } export default function DeviceDetailBox() { - const navigation = useNavigation(); - const navigate = useNavigate(); - const matches = useMatches(); - const { toast } = useToast(); + const navigation = useNavigation() + const navigate = useNavigate() + const matches = useMatches() + const { toast } = useToast() - const sensorIds = new Set(); + const sensorIds = new Set() - const data = useLoaderData(); - const nodeRef = useRef(null); - // state variables - const [open, setOpen] = useState(true); - const [offsetPositionX, setOffsetPositionX] = useState(0); - const [offsetPositionY, setOffsetPositionY] = useState(0); - const [compareMode, setCompareMode] = useGlobalCompareMode(); - const [refreshOn] = useState(false); - const [refreshSecond, setRefreshSecond] = useState(59); + const data = useLoaderData() + const nodeRef = useRef(null) + // state variables + const [open, setOpen] = useState(true) + const [offsetPositionX, setOffsetPositionX] = useState(0) + const [offsetPositionY, setOffsetPositionY] = useState(0) + const [compareMode, setCompareMode] = useGlobalCompareMode() + const [refreshOn] = useState(false) + const [refreshSecond, setRefreshSecond] = useState(59) - 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) - ); - setSensors(sortedSensors); - }, [data]); + const [sensors, setSensors] = useState() + useEffect(() => { + const sortedSensors = [...data.sensors].sort( + (a, b) => (a.id as unknown as number) - (b.id as unknown as number), + ) + setSensors(sortedSensors) + }, [data]) - const [searchParams] = useSearchParams(); + const [searchParams] = useSearchParams() - const { deviceId } = useParams(); // Get the deviceId from the URL params + const { deviceId } = useParams() // Get the deviceId from the URL params - const createSensorLink = (sensorIdToBeSelected: string) => { - const lastSegment = matches[matches.length - 1]?.params?.["*"]; - if (lastSegment) { - const secondLastSegment = matches[matches.length - 2]?.params?.sensorId; - sensorIds.add(secondLastSegment); - sensorIds.add(lastSegment); - } else { - const lastSegment = matches[matches.length - 1]?.params?.sensorId; - if (lastSegment) { - sensorIds.add(lastSegment); - } - } + const createSensorLink = (sensorIdToBeSelected: string) => { + const lastSegment = matches[matches.length - 1]?.params?.['*'] + if (lastSegment) { + const secondLastSegment = matches[matches.length - 2]?.params?.sensorId + sensorIds.add(secondLastSegment) + sensorIds.add(lastSegment) + } else { + const lastSegment = matches[matches.length - 1]?.params?.sensorId + if (lastSegment) { + sensorIds.add(lastSegment) + } + } - // If sensorIdToBeSelected is second selected sensor - if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 2) { - const clonedSet = new Set(sensorIds); - clonedSet.delete(sensorIdToBeSelected); - return `/explore/${deviceId}/${Array.from(clonedSet).join("/")}?${searchParams.toString()}`; - } else if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 1) { - return `/explore/${deviceId}?${searchParams.toString()}`; - } else if (sensorIds.size === 0) { - return `/explore/${deviceId}/${sensorIdToBeSelected}?${searchParams.toString()}`; - } else if (sensorIds.size === 1) { - return `/explore/${deviceId}/${Array.from(sensorIds).join("/")}/${sensorIdToBeSelected}?${searchParams.toString()}`; - } + // If sensorIdToBeSelected is second selected sensor + if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 2) { + const clonedSet = new Set(sensorIds) + clonedSet.delete(sensorIdToBeSelected) + return `/explore/${deviceId}/${Array.from(clonedSet).join('/')}?${searchParams.toString()}` + } else if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 1) { + return `/explore/${deviceId}?${searchParams.toString()}` + } else if (sensorIds.size === 0) { + return `/explore/${deviceId}/${sensorIdToBeSelected}?${searchParams.toString()}` + } else if (sensorIds.size === 1) { + return `/explore/${deviceId}/${Array.from(sensorIds).join('/')}/${sensorIdToBeSelected}?${searchParams.toString()}` + } - return ""; - }; + return '' + } - const isSensorActive = (sensorId: string) => { - if (sensorIds.has(sensorId)) { - return "bg-green-100 dark:bg-dark-green"; - } + const isSensorActive = (sensorId: string) => { + if (sensorIds.has(sensorId)) { + return 'bg-green-100 dark:bg-dark-green' + } - return "hover:bg-muted"; - }; + return 'hover:bg-muted' + } - function handleDrag(_e: any, data: DraggableData) { - setOffsetPositionX(data.x); - setOffsetPositionY(data.y); - } + function handleDrag(_e: any, data: DraggableData) { + setOffsetPositionX(data.x) + setOffsetPositionY(data.y) + } - const addLineBreaks = (text: string) => - text.split("\\n").map((text, index) => ( - - {text} -
-
- )); + const addLineBreaks = (text: string) => + text.split('\\n').map((text, index) => ( + + {text} +
+
+ )) - useEffect(() => { - let interval: any = null; - if (refreshOn) { - if (refreshSecond == 0) { - setRefreshSecond(59); - } - interval = setInterval(() => { - setRefreshSecond((refreshSecond) => refreshSecond - 1); - }, 1000); - } else if (!refreshOn) { - clearInterval(interval); - } - return () => clearInterval(interval); - }, [refreshOn, refreshSecond]); + useEffect(() => { + let interval: any = null + if (refreshOn) { + if (refreshSecond == 0) { + setRefreshSecond(59) + } + interval = setInterval(() => { + setRefreshSecond((refreshSecond) => refreshSecond - 1) + }, 1000) + } else if (!refreshOn) { + clearInterval(interval) + } + return () => clearInterval(interval) + }, [refreshOn, refreshSecond]) - if (!data.device) return null; + if (!data.device) return null - return ( - <> - {open && ( - } - defaultPosition={{ x: offsetPositionX, y: offsetPositionY }} - onDrag={handleDrag} - bounds="#osem" - handle="#deviceDetailBoxTop" - disabled={!isBrowser && !isTablet} - > -
-
- {navigation.state === "loading" && ( -
- -
- )} -
-
-
- {data.device.name} -
- - - - - - - Share this link - - - - Close - - - - - - - - - Actions - - - - Compare - - - - - - Archive - - - - - - - - External Link - - - - - + return ( + <> + {open && ( + } + defaultPosition={{ x: offsetPositionX, y: offsetPositionY }} + onDrag={handleDrag} + bounds="#osem" + handle="#deviceDetailBoxTop" + disabled={!isBrowser && !isTablet} + > +
+
+ {navigation.state === 'loading' && ( +
+ +
+ )} +
+
+
+ {data.device.name} +
+ + + + + + + Share this link + + + + Close + + + + + + + + + Actions + + + + Compare + + + + + + Archive + + + + + + + + External Link + + + + + - setOpen(false)} - /> - { - void navigate({ - pathname: "/explore", - search: searchParams.toString(), - }); - }} - /> -
-
-
-
- {data.device.image ? ( - device_image - ) : ( -
- -
- )} -
-
- - - - - - - {data.device.expiresAt && ( - <> - - - - )} -
-
- {data.device.tags && data.device.tags.length > 0 && ( -
-
-
- Tags -
-
- -
- {data.device.tags.map((tag: string) => ( - { - event.stopPropagation(); + setOpen(false)} + /> + { + void navigate({ + pathname: '/explore', + search: searchParams.toString(), + }) + }} + /> +
+
+
+
+ {data.device.image ? ( + device_image + ) : ( +
+ +
+ )} +
+
+ + + + + + + {data.device.expiresAt && ( + <> + + + + )} +
+
+ {data.device.tags && data.device.tags.length > 0 && ( +
+
+
+ Tags +
+
+ +
+ {data.device.tags.map((tag: string) => ( + { + event.stopPropagation() - const currentParams = new URLSearchParams( - searchParams.toString() - ); + const currentParams = new URLSearchParams( + searchParams.toString(), + ) - // Safely retrieve and parse the current tags - const currentTags = - currentParams.get("tags")?.split(",") || []; + // Safely retrieve and parse the current tags + const currentTags = + currentParams.get('tags')?.split(',') || [] - // Toggle the tag in the list - const updatedTags = currentTags.includes(tag) - ? currentTags.filter((t) => t !== tag) // Remove if already present - : [...currentTags, tag]; // Add if not present + // Toggle the tag in the list + const updatedTags = currentTags.includes(tag) + ? currentTags.filter((t) => t !== tag) // Remove if already present + : [...currentTags, tag] // Add if not present - // Update the tags parameter or remove it if empty - if (updatedTags.length > 0) { - currentParams.set( - "tags", - updatedTags.join(",") - ); - } else { - currentParams.delete("tags"); - } + // Update the tags parameter or remove it if empty + if (updatedTags.length > 0) { + currentParams.set( + 'tags', + updatedTags.join(','), + ) + } else { + currentParams.delete('tags') + } - // Update the URL with the new search params - void navigate({ - search: currentParams.toString(), - }); - }} - > - {tag} - - ))} -
-
-
-
- )} - - {data.device.logEntries.length > 0 && ( - <> - - - - )} - {data.device.description && ( - - - - Description - - - {addLineBreaks(data.device.description)} - - - - )} - - - - Sensors - - -
-
- {sensors && - sensors.map( - (sensor: SensorWithLatestMeasurement) => { - const sensorLink = createSensorLink(sensor.id); - if (sensorLink === "") { - return ( - - toast({ - title: - "Cant select more than 2 sensors", - description: - "Deselect one sensor to select another", - variant: "destructive", - }) - } - > - - - ); - } - return ( - - - - - - ); - } - )} -
-
-
-
-
-
-
-
- - )} - {compareMode && ( - - { - setCompareMode(!compareMode); - setOpen(true); - }} - /> - Compare devices - - Choose a device from the map to compare with. - - - )} - {!open && ( -
{ - 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 dark:bg-zinc-800 dark:text-zinc-200 dark:opacity-90 sm:bottom-[30px] sm:left-[10px]" - > - - - -
- -
-
- -

Open device details

-
-
-
-
- )} - - ); + // Update the URL with the new search params + void navigate({ + search: currentParams.toString(), + }) + }} + > + {tag} + + ))} +
+
+
+
+ )} + + {data.device.logEntries.length > 0 && ( + <> + + + + )} + {data.device.description && ( + + + + Description + + + {addLineBreaks(data.device.description)} + + + + )} + + + + Sensors + + +
+
+ {sensors && + sensors.map( + (sensor: SensorWithLatestMeasurement) => { + const sensorLink = createSensorLink(sensor.id) + if (sensorLink === '') { + return ( + + toast({ + title: + 'Cant select more than 2 sensors', + description: + 'Deselect one sensor to select another', + variant: 'destructive', + }) + } + > + + + ) + } + return ( + + + + + + ) + }, + )} +
+
+
+
+
+
+
+
+
+ )} + {compareMode && ( + + { + setCompareMode(!compareMode) + setOpen(true) + }} + /> + Compare devices + + Choose a device from the map to compare with. + + + )} + {!open && ( +
{ + 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 dark:bg-zinc-800 dark:text-zinc-200 dark:opacity-90 sm:bottom-[30px] sm:left-[10px]" + > + + + +
+ +
+
+ +

Open device details

+
+
+
+
+ )} + + ) } const InfoItem = ({ - icon: Icon, - title, - text, + icon: Icon, + title, + text, }: { - icon: React.ElementType; - title: string; - text?: string; + icon: React.ElementType + title: string + text?: string }) => - text && ( -
-
{title}
-
- - {text} -
-
- ); + text && ( +
+
{title}
+
+ + {text} +
+
+ ) diff --git a/app/routes/api.boxes.$deviceId.$sensorId.measurements.ts b/app/routes/api.boxes.$deviceId.$sensorId.measurements.ts new file mode 100644 index 00000000..a6299726 --- /dev/null +++ b/app/routes/api.boxes.$deviceId.$sensorId.measurements.ts @@ -0,0 +1,50 @@ +import { type ActionFunctionArgs, redirect } from 'react-router' +import { getUserFromJwt } from '~/lib/jwt' +import { getUserDevices } from '~/models/device.server' +import { deleteMeasurementsForSensor } from '~/models/measurement.server' +import { StandardResponse } from '~/utils/response-utils' + +export async function action({ request, params }: ActionFunctionArgs) { + try { + const { deviceId, sensorId } = params + if (!deviceId || !sensorId) + return StandardResponse.badRequest( + 'Invalid device id or sensor id specified', + ) + + const jwtResponse = await getUserFromJwt(request) + + if (typeof jwtResponse === 'string') + return StandardResponse.forbidden( + 'Invalid JWT authorization. Please sign in to obtain new JWT.', + ) + + if (request.method !== 'DELETE') + return StandardResponse.methodNotAllowed('Endpoint only supports DELETE') + + const userDevices = await getUserDevices(jwtResponse.id) + if (!userDevices.some((d) => d.id === deviceId)) + return StandardResponse.forbidden( + 'You are not allowed to delete data of the given device', + ) + + const device = userDevices.find((d) => d.id === deviceId) + if (!device?.sensors.some((s) => s.id === sensorId)) + return StandardResponse.forbidden( + 'You are not allowed to delete data of the given sensor', + ) + + // TODO add more parameters (from-date, to-date etc) + await deleteMeasurementsForSensor(sensorId) + return StandardResponse.ok({}) + } catch (err: any) { + return StandardResponse.internalServerError( + err.message || 'An unexpected error occured', + ) + } + + const MEASUREMENTS_ROUTE = `/api/boxes/${params.deviceId}/sensors/${params.sensorId}/measurements` + return redirect(MEASUREMENTS_ROUTE, { + status: 308, // Permanent Redirect + }) +} diff --git a/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts b/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts index 89b38d09..b6fcf6a6 100644 --- a/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts +++ b/app/routes/api.boxes.$deviceId.sensors.$sensorId.ts @@ -1,37 +1,44 @@ -import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; -import { getLatestMeasurementsForSensor } from "~/lib/measurement-service.server"; -import { StandardResponse } from "~/utils/response-utils"; +import { type LoaderFunction, type LoaderFunctionArgs } from 'react-router' +import { getLatestMeasurementsForSensor } from '~/lib/measurement-service.server' +import { StandardResponse } from '~/utils/response-utils' export const loader: LoaderFunction = async ({ - request, - params, + request, + params, }: LoaderFunctionArgs): Promise => { - try { - const deviceId = params.deviceId; - if (deviceId === undefined) - return StandardResponse.badRequest("Invalid device id specified"); + try { + const deviceId = params.deviceId + if (deviceId === undefined) + return StandardResponse.badRequest('Invalid device id specified') - const sensorId = params.sensorId; - if (sensorId === undefined) - return StandardResponse.badRequest("Invalid sensor id specified"); + const sensorId = params.sensorId + if (sensorId === undefined) + return StandardResponse.badRequest('Invalid sensor id specified') - const searchParams = new URL(request.url).searchParams; - const onlyValue = - (searchParams.get("onlyValue")?.toLowerCase() ?? "") === "true"; - if (sensorId === undefined && onlyValue) - return StandardResponse.badRequest("onlyValue can only be used when a sensor id is specified"); + const searchParams = new URL(request.url).searchParams + const onlyValue = + (searchParams.get('onlyValue')?.toLowerCase() ?? '') === 'true' + if (sensorId === undefined && onlyValue) + return StandardResponse.badRequest( + 'onlyValue can only be used when a sensor id is specified', + ) - const meas = await getLatestMeasurementsForSensor(deviceId, sensorId, undefined); + const meas = await getLatestMeasurementsForSensor( + deviceId, + sensorId, + undefined, + ) - if (meas == null) - return StandardResponse.notFound("Device not found."); + if (meas == null) return StandardResponse.notFound('Device not found.') - if (onlyValue) - return StandardResponse.ok(meas["lastMeasurement"]?.value ?? null); + if (onlyValue) + return StandardResponse.ok(meas['lastMeasurement']?.value ?? null) - return StandardResponse.ok({ ...meas, _id: meas.id } /* for legacy purposes */); - } catch (err) { - console.warn(err); - return StandardResponse.internalServerError(); - } -}; + return StandardResponse.ok( + { ...meas, _id: meas.id } /* for legacy purposes */, + ) + } catch (err) { + console.warn(err) + return StandardResponse.internalServerError() + } +} diff --git a/app/routes/api.boxes.$deviceId.sensors.ts b/app/routes/api.boxes.$deviceId.sensors.ts index 6fd11237..fade97c9 100644 --- a/app/routes/api.boxes.$deviceId.sensors.ts +++ b/app/routes/api.boxes.$deviceId.sensors.ts @@ -1,6 +1,6 @@ -import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; -import { getLatestMeasurements } from "~/lib/measurement-service.server"; -import { StandardResponse } from "~/utils/response-utils"; +import { type LoaderFunction, type LoaderFunctionArgs } from 'react-router' +import { getLatestMeasurements } from '~/lib/measurement-service.server' +import { StandardResponse } from '~/utils/response-utils' /** * @openapi @@ -31,28 +31,30 @@ import { StandardResponse } from "~/utils/response-utils"; */ export const loader: LoaderFunction = async ({ - request, - params, + request, + params, }: LoaderFunctionArgs): Promise => { - try { - const deviceId = params.deviceId; - if (deviceId === undefined) - return StandardResponse.badRequest("Invalid device id specified"); + try { + const deviceId = params.deviceId + if (deviceId === undefined) + return StandardResponse.badRequest('Invalid device id specified') - const url = new URL(request.url); - const countParam = url.searchParams.get("count"); + const url = new URL(request.url) + const countParam = url.searchParams.get('count') - let count: undefined | number = undefined; - if (countParam !== null && Number.isNaN(countParam)) - return StandardResponse.badRequest("Illegal value for parameter count. allowed values: numbers"); + let count: undefined | number = undefined + if (countParam !== null && Number.isNaN(countParam)) + return StandardResponse.badRequest( + 'Illegal value for parameter count. allowed values: numbers', + ) - count = countParam === null ? undefined : Number(countParam); + count = countParam === null ? undefined : Number(countParam) - const meas = await getLatestMeasurements(deviceId, count); + const meas = await getLatestMeasurements(deviceId, count) - return StandardResponse.ok(meas); - } catch (err) { - console.warn(err); - return StandardResponse.internalServerError(); - } -}; + return StandardResponse.ok(meas) + } catch (err) { + console.warn(err) + return StandardResponse.internalServerError() + } +} diff --git a/app/routes/api.ts b/app/routes/api.ts index 2a15d59d..a2a91a25 100644 --- a/app/routes/api.ts +++ b/app/routes/api.ts @@ -1,224 +1,235 @@ -import { type LoaderFunctionArgs } from "react-router"; +import { type LoaderFunctionArgs } from 'react-router' -type RouteInfo = { path: string; method: "GET" | "PUT" | "POST" | "DELETE" }; +type RouteInfo = { + path: string + method: 'GET' | 'PUT' | 'POST' | 'DELETE' + deprecationNotice?: string +} const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { - noauth: [ - { - path: "/", - method: "GET", - }, - { - path: "/stats", - method: "GET", - }, - { - path: "/tags", - method: "GET", - }, - // { - // path: `statistics/idw`, - // method: "GET", + noauth: [ + { + path: '/', + method: 'GET', + }, + { + path: '/stats', + method: 'GET', + }, + { + path: '/tags', + method: 'GET', + }, + // { + // path: `statistics/idw`, + // method: "GET", - // }, - // { - // path: `statistics/descriptive`, - // method: "GET", + // }, + // { + // path: `statistics/descriptive`, + // method: "GET", - // }, - { - path: `boxes`, - method: "GET", - }, - // { - // path: `boxes/data`, - // method: "GET", - // }, + // }, + { + path: `boxes`, + method: 'GET', + }, + // { + // path: `boxes/data`, + // method: "GET", + // }, - // { - // path: `boxes/:boxId`, - // method: "GET", - // }, - { - path: `boxes/:boxId/sensors`, - method: "GET", - }, - { - path: `boxes/:boxId/sensors/:sensorId`, - method: "GET", - }, - // { - // path: `boxes/:boxId/data/:sensorId`, - // method: "GET", - // }, - // { - // path: `boxes/:boxId/locations`, - // method: "GET", - // }, - // { - // path: `boxes/data`, - // method: "POST", - // }, - { - path: `boxes/:boxId/data`, - method: "POST", - }, - { - path: `boxes/:boxId/:sensorId`, - method: "POST", - }, - { - path: `users/register`, - method: "POST", - }, - { - path: `users/request-password-reset`, - method: "POST", - }, - { - path: `users/password-reset`, - method: "POST", - }, - { - path: `users/confirm-email`, - method: "POST", - }, - { - path: `users/sign-in`, - method: "POST", - }, - ], - auth: [ - { - path: `users/refresh-auth`, - method: "POST", - }, - { - path: `users/me`, - method: "GET", - }, - { - path: `users/me`, - method: "PUT", - }, - { - path: `users/me/boxes`, - method: "GET", - }, - { - path: `users/me/boxes/:boxId`, - method: "GET", - }, - // { - // path: `boxes/:boxId/script`, - // method: "GET", - // }, - { - path: `boxes`, - method: "POST", - }, - { - path: `boxes/claim`, - method: "POST", - }, - { - path: `boxes/transfer`, - method: "POST", - }, - { - path: `boxes/transfer`, - method: "DELETE", - }, - { - path: `boxes/transfer/:boxId`, - method: "GET", - }, - { - path: `boxes/transfer/:boxId`, - method: "PUT", - }, - // { - // path: `boxes/:boxId`, - // method: "PUT", - // }, - { - path: `boxes/:boxId`, - method: "DELETE", - }, - // { - // path: `boxes/:boxId/:sensorId/measurements`, - // method: "DELETE", - // }, - { - path: `users/sign-out`, - method: "POST", - }, - { - path: `users/me`, - method: "DELETE", - }, - { - path: `users/me/resend-email-confirmation`, - method: "POST", - }, - ], - // management: [ - // { - // path: `${managementPath}/boxes`, - // method: "GET", - // }, - // { - // path: `${managementPath}/boxes/:boxId`, - // method: "GET", - // }, - // { - // path: `${managementPath}/boxes/:boxId`, - // method: "PUT", - // }, - // { - // path: `${managementPath}/boxes/delete`, - // method: "POST", - // }, - // { - // path: `${managementPath}/users`, - // method: "GET", - // }, - // { - // path: `${managementPath}/users/:userId`, - // method: "GET", - // }, - // { - // path: `${managementPath}/users/:userId`, - // method: "PUT", - // }, - // { - // path: `${managementPath}/users/delete`, - // method: "POST", - // }, - // { - // path: `${managementPath}/users/:userId/exec`, - // method: "POST", - // }, - // ], -}; + // { + // path: `boxes/:boxId`, + // method: "GET", + // }, + { + path: `boxes/:boxId/sensors`, + method: 'GET', + }, + { + path: `boxes/:boxId/sensors/:sensorId`, + method: 'GET', + }, + // { + // path: `boxes/:boxId/data/:sensorId`, + // method: "GET", + // }, + // { + // path: `boxes/:boxId/locations`, + // method: "GET", + // }, + // { + // path: `boxes/data`, + // method: "POST", + // }, + { + path: `boxes/:boxId/data`, + method: 'POST', + }, + { + path: `boxes/:boxId/:sensorId`, + method: 'POST', + }, + { + path: `users/register`, + method: 'POST', + }, + { + path: `users/request-password-reset`, + method: 'POST', + }, + { + path: `users/password-reset`, + method: 'POST', + }, + { + path: `users/confirm-email`, + method: 'POST', + }, + { + path: `users/sign-in`, + method: 'POST', + }, + ], + auth: [ + { + path: `users/refresh-auth`, + method: 'POST', + }, + { + path: `users/me`, + method: 'GET', + }, + { + path: `users/me`, + method: 'PUT', + }, + { + path: `users/me/boxes`, + method: 'GET', + }, + { + path: `users/me/boxes/:boxId`, + method: 'GET', + }, + // { + // path: `boxes/:boxId/script`, + // method: "GET", + // }, + { + path: `boxes`, + method: 'POST', + }, + { + path: `boxes/claim`, + method: 'POST', + }, + { + path: `boxes/transfer`, + method: 'POST', + }, + { + path: `boxes/transfer`, + method: 'DELETE', + }, + { + path: `boxes/transfer/:boxId`, + method: 'GET', + }, + { + path: `boxes/transfer/:boxId`, + method: 'PUT', + }, + // { + // path: `boxes/:boxId`, + // method: "PUT", + // }, + { + path: `boxes/:boxId`, + method: 'DELETE', + }, + { + path: `boxes/:boxId/:sensorId/measurements`, + method: 'DELETE', + }, + { + path: `boxes/:boxId/sensors/:sensorId/measurements`, + method: 'DELETE', + }, + { + path: `users/sign-out`, + method: 'POST', + }, + { + path: `users/me`, + method: 'DELETE', + }, + { + path: `users/me/resend-email-confirmation`, + method: 'POST', + }, + ], + // management: [ + // { + // path: `${managementPath}/boxes`, + // method: "GET", + // }, + // { + // path: `${managementPath}/boxes/:boxId`, + // method: "GET", + // }, + // { + // path: `${managementPath}/boxes/:boxId`, + // method: "PUT", + // }, + // { + // path: `${managementPath}/boxes/delete`, + // method: "POST", + // }, + // { + // path: `${managementPath}/users`, + // method: "GET", + // }, + // { + // path: `${managementPath}/users/:userId`, + // method: "GET", + // }, + // { + // path: `${managementPath}/users/:userId`, + // method: "PUT", + // }, + // { + // path: `${managementPath}/users/delete`, + // method: "POST", + // }, + // { + // path: `${managementPath}/users/:userId/exec`, + // method: "POST", + // }, + // ], +} export async function loader({}: LoaderFunctionArgs) { - const lines = [ - `This is the openSenseMap API`, - "You can find a detailed reference at https://docs.opensensemap.org\n", - "Routes requiring no authentication:", - ]; + const lines = [ + `This is the openSenseMap API`, + 'You can find a detailed reference at https://docs.opensensemap.org\n', + 'Routes requiring no authentication:', + ] - for (const r of routes.noauth) lines.push(`${r.method}\t${r.path}`); + for (const r of routes.noauth) lines.push(`${r.method}\t${r.path}`) - lines.push("\nRoutes requiring valid authentication through JWT:"); + lines.push('\nRoutes requiring valid authentication through JWT:') - for (const r of routes.auth) lines.push(`${r.method}\t${r.path}`); + for (const r of routes.auth) + lines.push( + `${r.method}\t${r.path}\t${r.deprecationNotice ? 'DEPRECATED: ' + r.deprecationNotice : ''}`, + ) - return new Response(lines.join("\n"), { - status: 200, - headers: { - "Content-Type": "text/plain; charset=utf-8", - }, - }); + return new Response(lines.join('\n'), { + status: 200, + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + }, + }) }