Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
dc5e597
feat: add integrations schema
jona159 Jan 4, 2026
30b1d4e
feat: add internal endpoint to post measurements via mqtt
jona159 Jan 4, 2026
53163a2
feat: mqtt server functions
jona159 Jan 4, 2026
cd09724
feat: endpoint for active mqtt configurations
jona159 Jan 4, 2026
2a2869b
fix: make createdAt mandatory to preserver history for batch measurem…
jona159 Jan 8, 2026
4e65d7d
feat: add mqtt client
jona159 Jan 9, 2026
7ac3a31
feat: mqtt routes and db methods
jona159 Jan 9, 2026
24fd9c7
feat: add mqtt service url and key to env
jona159 Jan 9, 2026
92eddcb
feat: install json to form package and define base widgets for styling
jona159 Feb 3, 2026
884a16c
fix: check if measurement is posted by mqtt service
jona159 Feb 3, 2026
0fdec95
feat: form to edit mqtt config from json schema provided by mqtt service
jona159 Feb 3, 2026
a25d0a5
feat: add route to fetch schema from mqtt service
jona159 Feb 4, 2026
9323e3a
feat: replace mqtt form with form generated from json schema
jona159 Feb 4, 2026
6f2bc26
fix: adjust validation for new mqtt form
jona159 Feb 4, 2026
d377ec3
feat: add more widgets
jona159 Feb 4, 2026
460c134
feat: save mqtt config in mqtt service db when creating device#
jona159 Feb 4, 2026
4a7565d
feat: rm mqtt specific apis
jona159 Feb 6, 2026
25e47b3
feat: install openapi-types, seed integrations
jona159 Feb 6, 2026
ffc4d71
feat: adjust integration schema
jona159 Feb 6, 2026
ee9a306
feat: integration db functions
jona159 Feb 6, 2026
5e8c7c5
feat: create integrations via generic api routes
jona159 Feb 6, 2026
f787108
feat: show integrations dynamically in sidebar
jona159 Feb 6, 2026
6d6962a
feat:icon map
jona159 Feb 6, 2026
3c59032
feat: document integrations
jona159 Feb 6, 2026
d05f3d2
fix: minor adjustments
jona159 Feb 11, 2026
7764e12
feat: edit ttn config
jona159 Feb 11, 2026
873d328
Merge branch 'dev' into feat/mqtt-json-schema
jona159 Feb 11, 2026
73c7700
fix: migration and deps conflicts
jona159 Feb 11, 2026
302eee7
fix: rm unused files
jona159 Feb 17, 2026
ca7dae4
fix: rm stuff
jona159 Feb 17, 2026
3ff9954
fix: seed integrations
jona159 Feb 17, 2026
d66fe69
fix: rename service key
jona159 Feb 17, 2026
a77ff82
feat: integration service
jona159 Feb 17, 2026
062293a
fix: order not nullable
jona159 Feb 17, 2026
c240ab7
feat: load integrations in root loader and pass down to step
jona159 Feb 17, 2026
e0bd053
fix: general integratoin edit component
jona159 Feb 17, 2026
c95988c
fix: rm generated files and add to gitignore
jona159 Feb 17, 2026
5a73479
feat: support deep linking
jona159 Feb 17, 2026
66070b1
feat: improve docs
jona159 Feb 17, 2026
8ecc26d
fix: rm from gitignore
jona159 Feb 17, 2026
bce8dca
feat: add step to generate docs
jona159 Feb 17, 2026
adc399e
fix: command
jona159 Feb 17, 2026
cc1b7f8
Merge branch 'dev' into feat/mqtt-json-schema
jona159 Feb 17, 2026
38041bb
fix: dont pin buildx
jona159 Feb 18, 2026
917a712
fix: replace import with fetch
jona159 Feb 18, 2026
5cd2800
feat: bypass device authentication for requests from trusted services
jona159 Feb 18, 2026
b1554db
feat: readd script for seeding integrations
jona159 Feb 20, 2026
f7598ca
Merge branch 'dev' into feat/mqtt-json-schema
jona159 Feb 20, 2026
89588f1
fix: device auth check only if not trusted service
jona159 Feb 20, 2026
025600f
fix: rm help text
jona159 Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ OSEM_API_URL="https://api.opensensemap.org/"
DIRECTUS_URL="https://coelho.opensensemap.org"
SENSORWIKI_API_URL="https://api.sensors.wiki/"

MQTT_SERVICE_URL="http://localhost:3001"
MQTT_SERVICE_KEY="dev-service-key-change-in-production"

MYBADGES_API_URL = "https://api.v2.mybadges.org/"
MYBADGES_URL = "https://mybadges.org/"
MYBADGES_SERVERADMIN_USERNAME = ""
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ jobs:
- name: 📥 Install deps
run: npm install

- name: 🧾 Generate OpenAPI spec
run: npm run build:docs

- name: 🔎 Type check
run: npm run typecheck --if-present

Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ measurements.csv

/coverage

/minio-data
/minio-data

/public/openapi.json
365 changes: 121 additions & 244 deletions app/components/device/new/advanced-info.tsx
Original file line number Diff line number Diff line change
@@ -1,246 +1,123 @@
import { useFormContext } from 'react-hook-form'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '~/components/ui/card'
import { Input } from '~/components/ui/input'
import { Label } from '~/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '~/components/ui/select'
import { Switch } from '~/components/ui/switch'
import { Textarea } from '~/components/ui/textarea'

export function AdvancedStep() {
const { register, setValue, watch, resetField } = useFormContext()

// Watch field states
const isMqttEnabled = watch('mqttEnabled') || false
const isTtnEnabled = watch('ttnEnabled') || false

// Clear corresponding fields when disabling
const handleMqttToggle = (checked: boolean) => {
setValue('mqttEnabled', checked)
if (!checked) {
resetField('url')
resetField('topic')
resetField('messageFormat')
resetField('decodeOptions')
resetField('connectionOptions')
}
}

const handleTtnToggle = (checked: boolean) => {
setValue('ttnEnabled', checked)
if (!checked) {
resetField('dev_id')
resetField('app_id')
resetField('profile')
resetField('decodeOptions')
resetField('port')
}
}

const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = event.target
setValue(name, value)
}

const handleSelectChange = (field: string, value: string) => {
setValue(field, value)
}

return (
<>
{/* MQTT Configuration */}
<Card className="w-full">
<CardHeader>
<CardTitle>MQTT Configuration</CardTitle>
<CardDescription>
Configure your MQTT settings for data streaming
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="mqttEnabled" className="text-base font-semibold">
Enable MQTT
</Label>
<Switch
disabled
id="mqttEnabled"
checked={isMqttEnabled}
onCheckedChange={handleMqttToggle}
/>
</div>

{isMqttEnabled && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="mqtt-url">MQTT URL</Label>
<Input
id="mqtt-url"
placeholder="mqtt://example.com:1883"
{...register('url')}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-topic">MQTT Topic</Label>
<Input
id="mqtt-topic"
placeholder="my/mqtt/topic"
{...register('topic')}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-message-format">Message Format</Label>
<Select
onValueChange={(value) =>
handleSelectChange('messageFormat', value)
}
defaultValue={watch('messageFormat')}
>
<SelectTrigger id="mqtt-message-format">
<SelectValue placeholder="Select a message format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="csv">CSV</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-decode-options">Decode Options</Label>
<Textarea
id="mqtt-decode-options"
placeholder="Enter decode options as JSON"
className="resize-none"
{...register('decodeOptions')}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="mqtt-connection-options">
Connection Options
</Label>
<Textarea
id="mqtt-connection-options"
placeholder="Enter connection options as JSON"
className="resize-none"
{...register('connectionOptions')}
onChange={handleInputChange}
/>
</div>
</div>
)}
</CardContent>
</Card>

{/* TTN Configuration */}
<Card className="mt-6 w-full">
<CardHeader>
<CardTitle>TTN Configuration</CardTitle>
<CardDescription>
Configure your TTN (The Things Network) settings
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="ttnEnabled" className="text-base font-semibold">
Enable TTN
</Label>
<Switch
disabled
id="ttnEnabled"
checked={isTtnEnabled}
onCheckedChange={handleTtnToggle}
/>
</div>

{isTtnEnabled && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ttn-dev-id">Device ID</Label>
<Input
id="ttn-dev-id"
placeholder="Enter TTN Device ID"
{...register('dev_id')}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-app-id">Application ID</Label>
<Input
id="ttn-app-id"
placeholder="Enter TTN Application ID"
{...register('app_id')}
onChange={handleInputChange}
/>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-profile">Profile</Label>
<Select
onValueChange={(value) =>
handleSelectChange('profile', value)
}
defaultValue={watch('profile')}
>
<SelectTrigger id="ttn-profile">
<SelectValue placeholder="Select a profile" />
</SelectTrigger>
<SelectContent>
<SelectItem value="lora-serialization">
Lora Serialization
</SelectItem>
<SelectItem value="sensebox/home">Sensebox/Home</SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="debug">Debug</SelectItem>
<SelectItem value="cayenne-lpp">Cayenne LPP</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="ttn-decode-options">Decode Options</Label>
<Textarea
id="ttn-decode-options"
placeholder="Enter decode options as JSON"
className="resize-none"
{...register('decodeOptions')}
onChange={handleInputChange}
/>
</div>
import Form from "@rjsf/core";
import validator from "@rjsf/validator-ajv8";
import { useEffect, useState } from "react";

Check warning on line 3 in app/components/device/new/advanced-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Lint

'useEffect' is defined but never used. Allowed unused vars must match /^ignored/u
import { useFormContext } from "react-hook-form";
import { CheckboxWidget } from "~/components/rjsf/checkboxWidget";
import { FieldTemplate } from "~/components/rjsf/fieldTemplate";
import { BaseInputTemplate } from "~/components/rjsf/inputTemplate";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Label } from "~/components/ui/label";
import { Switch } from "~/components/ui/switch";

interface Integration {
id: string;
name: string;
slug: string;
icon?: string | null;
description?: string | null;
order: number;
}

<div className="space-y-2">
<Label htmlFor="ttn-port">Port</Label>
<Input
id="ttn-port"
placeholder="Enter TTN Port"
type="number"
{...register('port', { valueAsNumber: true })}
onChange={handleInputChange}
/>
</div>
</div>
)}
</CardContent>
</Card>
</>
)
interface AdvancedStepProps {
integrations: Integration[];
}

export function AdvancedStep({ integrations }: AdvancedStepProps) {
const { watch, setValue, resetField } = useFormContext();
const [schemas, setSchemas] = useState<Record<string, { schema: any; uiSchema: any }>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});


const loadSchema = async (slug: string) => {
if (schemas[slug]) return;

setLoading((prev) => ({ ...prev, [slug]: true }));

try {
const res = await fetch(`/api/integrations/schema/${slug}`);
if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`);

const data = await res.json();
setSchemas((prev) => ({ ...prev, [slug]: data }));
} catch (err) {
console.error(`Failed to load ${slug} schema`, err);
} finally {
setLoading((prev) => ({ ...prev, [slug]: false }));
}
}

const handleToggle = (slug: string, checked: boolean) => {
setValue(`${slug}Enabled`, checked);

if (checked) {
void loadSchema(slug);
} else {
resetField(`${slug}Config`);
}
};

return (
<>
{integrations.map((intg) => {
const enabled = watch(`${intg.slug}Enabled`) ?? false;
const config = watch(`${intg.slug}Config`) ?? {};
const isLoading = loading[intg.slug] ?? false;
const schema = schemas[intg.slug];

return (
<Card key={intg.id} className="w-full mb-6">
<CardHeader>
<CardTitle>{intg.name} Configuration</CardTitle>
{intg.description && (
<CardDescription>{intg.description}</CardDescription>
)}
</CardHeader>

<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor={`${intg.slug}Enabled`} className="text-base font-semibold">
Enable {intg.name}
</Label>
<Switch
id={`${intg.slug}Enabled`}
checked={enabled}
onCheckedChange={(checked) => handleToggle(intg.slug, checked)}
/>
</div>

{enabled && (
<>
{isLoading && (
<p className="text-sm text-muted-foreground">
Loading {intg.name} configuration…
</p>
)}

{schema && (
<Form
widgets={{ CheckboxWidget }}
templates={{ FieldTemplate, BaseInputTemplate }}
schema={schema.schema}
uiSchema={schema.uiSchema}
validator={validator}
formData={config}
onChange={(e) => {
setValue(`${intg.slug}Config`, e.formData, {
shouldDirty: true,
shouldValidate: true,
});
}}
onSubmit={() => {}}
>
<></>
</Form>
)}
</>
)}
</CardContent>
</Card>
);
})}
</>
);
}
Loading
Loading