frontend is fucked

This commit is contained in:
Jono Targett 2026-03-15 23:05:23 +10:30
parent dbf511f27c
commit ba0b5cdf5c
3 changed files with 140 additions and 92 deletions

View File

@ -1,119 +1,85 @@
<template>
<div class="p-4">
<div class="p-grid">
<!-- Devices Panel -->
<div class="p-col-12 p-md-3">
<Panel header="Devices">
<div class="p-grid p-m-2">
<div class="p-col-12 p-md-3">
<div class="p-panel p-shadow-3">
<div class="p-panel-header">Devices</div>
<div class="p-panel-content">
<ul>
<li v-for="(device, id) in devices" :key="id">
<Button
:label="id + ' — ' + device.status"
:class="{'p-button-outlined': selected !== id}"
<button
class="p-button p-button-text"
@click="selectDevice(id)"
/>
>
{{ id }}
</button>
</li>
</ul>
</Panel>
</div>
</div>
</div>
<!-- Properties & Commands Panel -->
<div class="p-col-12 p-md-9" v-if="selected">
<Panel :header="'Properties — ' + selected" class="mb-3">
<Tree :value="treeData" />
</Panel>
<Panel :header="'Commands — ' + selected">
<Button
v-for="(cmd, name) in devices[selected].commands"
:key="name"
class="p-m-2"
:label="name"
@click="sendCommand(name)"
/>
</Panel>
<div class="p-col-12 p-md-9" v-if="selected">
<div class="p-panel p-shadow-3">
<div class="p-panel-header">Properties</div>
<div class="p-panel-content">
<PropertyTree :properties="devices[selected].property" />
</div>
</div>
</div>
</div>
</template>
<script>
import MQTTService from './services/mqtt'
import { reactive } from 'vue'
import MQTTService from './services/mqtt.js'
import PropertyTree from './PropertyTree.vue'
export default {
components: { PropertyTree },
data() {
return {
mqtt: null,
devices: {}, // { deviceId: {status, properties, commands} }
devices: reactive({}),
selected: null,
treeData: [],
}
},
mounted() {
this.mqtt = new MQTTService()
// Device status
this.mqtt.subscribe('asset/+/status', (payload, topic) => {
const id = topic.split('/')[1]
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
this.devices[id].status = payload
})
// Device properties
this.mqtt.subscribe('asset/+/property/#', (payload, topic) => {
const parts = topic.split('/')
const id = parts[1]
const keyPath = parts.slice(3)
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
let target = this.devices[id].properties
keyPath.forEach((k, idx) => {
if (idx === keyPath.length - 1) {
try { target[k] = JSON.parse(payload) } catch(e) { target[k] = payload }
} else {
if (!target[k] || typeof target[k] !== 'object') this.$set(target, k, {})
target = target[k]
}
})
if (this.selected) this.buildTree()
})
// Device commands
this.mqtt.subscribe('asset/+/command/#', (payload, topic) => {
const id = topic.split('/')[1]
const cmdName = topic.split('/')[3]
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
try { this.$set(this.devices[id].commands, cmdName, JSON.parse(payload)) }
catch(e) { this.$set(this.devices[id].commands, cmdName, { raw: payload }) }
this.mqtt.client.on('connect', () => {
console.log('Connected to MQTT broker')
this.mqtt.subscribe('asset/+/status', this.handleStatus)
this.mqtt.subscribe('asset/+/property/#', this.handleProperty)
})
},
methods: {
selectDevice(id) {
this.selected = id
this.buildTree()
},
sendCommand(cmdName) {
const topic = `asset/${this.selected}/command/${cmdName}`
this.mqtt.publish(topic, JSON.stringify({args:{}}))
handleStatus(payload, topic) {
const deviceId = topic.split('/')[1]
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
this.devices[deviceId].status = payload
},
buildTree() {
const buildNode = (obj, label) => {
if (typeof obj === 'object' && obj !== null) {
return { label, children: Object.entries(obj).map(([k,v]) => buildNode(v,k)) }
handleProperty(payload, topic) {
const parts = topic.split('/')
const deviceId = parts[1]
const propertyPath = parts.slice(3) // after asset/device/property/...
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
// traverse/create tree
let node = this.devices[deviceId].property
propertyPath.forEach((p, i) => {
if (i === propertyPath.length - 1) {
// leaf
node[p] = { value: payload, updated: Date.now() } // flash on update
} else {
return { label: `${label}: ${obj}` }
if (!node[p]) node[p] = {}
node = node[p]
}
}
this.treeData = Object.entries(this.devices[this.selected].properties).map(([k,v]) => buildNode(v,k))
}
}
})
},
},
}
</script>
<style>
.p-m-2 { margin: .5rem; }
.p-4 { padding: 1rem; }
.mb-3 { margin-bottom: 1rem; }
</style>

View File

@ -0,0 +1,70 @@
<template>
<Tree
:value="treeData"
:expansion-keys="expansionKeys"
>
<template #default="{ node }">
<span
:style="{
backgroundColor: recentlyUpdated(node) ? '#fffa90' : 'transparent',
padding: '0 2px',
borderRadius: '2px'
}"
>
{{ node.label }}
</span>
</template>
</Tree>
</template>
<script>
import Tree from 'primevue/tree'
export default {
name: 'PropertyTree',
components: { Tree },
props: {
properties: { type: Object, required: true },
},
data() {
return {
treeData: [],
expansionKeys: {},
}
},
watch: {
properties: {
deep: true,
immediate: true,
handler() {
this.treeData = this.buildTree(this.properties)
},
},
},
methods: {
buildTree(obj, path = []) {
return Object.entries(obj).map(([key, val]) => {
if (val && typeof val === 'object' && 'value' in val) {
return {
key: [...path, key].join('/'),
label: `${key}: ${val.value}`,
data: val,
}
} else if (val && typeof val === 'object') {
const children = this.buildTree(val, [...path, key])
this.expansionKeys[[...path, key].join('/')] = true
return {
key: [...path, key].join('/'),
label: key,
children,
}
}
})
},
recentlyUpdated(node) {
const updated = node.data?.updated
return updated && Date.now() - updated < 500
},
},
}
</script>

View File

@ -1,19 +1,24 @@
import mqtt from 'mqtt'
export default class MQTTService {
constructor(brokerUrl = 'ws://localhost:8083/mqtt', clientId = null) {
constructor(brokerUrl = 'ws://127.0.0.1:8083/mqtt', clientId = null) {
this.clientId = clientId || 'vue-client-' + Math.random().toString(16).substr(2, 8)
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId })
this.subscriptions = {}
this.subscriptions = [] // array of {topic, callback}
this.client.on('connect', () => console.log('Connected to MQTT broker'))
this.client.on('message', (topic, payload) => {
if (this.subscriptions[topic]) this.subscriptions[topic](payload.toString(), topic)
// iterate over subscriptions and check for matches
this.subscriptions.forEach(({topic: subTopic, callback}) => {
if (mqttMatch(subTopic, topic)) {
callback(payload.toString(), topic)
}
})
})
}
subscribe(topic, callback) {
this.subscriptions[topic] = callback
this.subscriptions.push({topic, callback})
this.client.subscribe(topic)
}
@ -21,3 +26,10 @@ export default class MQTTService {
this.client.publish(topic, message, options)
}
}
// helper function for MQTT wildcards
function mqttMatch(subTopic, topic) {
// replace MQTT wildcards with RegExp
const regex = '^' + subTopic.replace('+', '[^/]+').replace('#', '.+') + '$'
return new RegExp(regex).test(topic)
}