Merge branch 'staging_environment' into get_permissions_fix

This commit is contained in:
csawatzky 2026-03-26 14:30:46 -06:00
commit 777089e8c4
65 changed files with 1144 additions and 1773 deletions

View file

@ -63,7 +63,7 @@ import {
} from "providers";
import React, { SetStateAction, useCallback, useEffect, useState } from "react";
// import { getThemeType } from "theme";
import { getGrainUnit, stringToMaterialColour } from "utils";
import { stringToMaterialColour } from "utils";
//import InfiniteScroll from "react-infinite-scroller";
import PageContainer from "./PageContainer";
import ObjectTable from "objects/ObjectTable";
@ -934,7 +934,7 @@ export default function Bins(props: Props) {
return " bu";
}
switch (getGrainUnit()) {
switch (user.grainUnit()) {
case pond.GrainUnit.GRAIN_UNIT_TONNE:
return " mT";
case pond.GrainUnit.GRAIN_UNIT_TON:
@ -947,9 +947,9 @@ export default function Bins(props: Props) {
const customQuantityDisplay = (customInventory: pond.CustomInventory, bushels: number) => {
let amount = bushels
if(customInventory.bushelsPerTonne > 1){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/customInventory.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/(customInventory.bushelsPerTonne*0.907)
}
}
@ -959,9 +959,9 @@ export default function Bins(props: Props) {
const supportedGrainDisplay = (grain: pond.Grain, bushels: number) => {
const describer = GrainDescriber(grain)
let amount = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/describer.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/describer.bushelsPerTon
}
return Math.round(amount*100)/100

View file

@ -11,7 +11,7 @@ import PageContainer from "./PageContainer";
export default function Contracts() {
const [openSettings, setOpenSettings] = useState(false);
const [{ as }] = useGlobalState()
const [{ as, user }] = useGlobalState()
const contractAPI = useContractAPI();
const [contracts, setContracts] = useState<Contract[]>([]);
const [loading, setLoading] = useState(false);
@ -42,7 +42,7 @@ export default function Contracts() {
let contractPermissions: Map<string, pond.Permission[]> = new Map();
if (resp.data.contracts){
resp.data.contracts.forEach(contract => {
let c = Contract.create(contract);
let c = Contract.create(contract, user);
contracts.push(c);
let p = pond.EvaluatePermissionsResponse.fromObject(
resp.data.contractPermissions[c.key()]
@ -56,7 +56,7 @@ export default function Contracts() {
})
.catch(err => {});
}
}, [contractAPI, year, as]); // eslint-disable-line react-hooks/exhaustive-deps
}, [contractAPI, year, as, user]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<PageContainer>

View file

@ -3,7 +3,8 @@ import Grid from '@mui/material/Grid2';
import { Component, Device, Interaction, User } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import { useEffect, useState } from "react";
import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useHTTP, useMobile } from "hooks";
@ -23,6 +24,25 @@ import DeviceWizard from "device/DeviceWizard";
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
import { useWebSocket } from "hooks/useWebSocket";
type TimestampedReading = { timestamp?: string };
function getTimestampMillis(timestamp?: string): number | undefined {
if (!timestamp) return undefined;
const parsed = Date.parse(timestamp);
return Number.isNaN(parsed) ? undefined : parsed;
}
function pickNewestReading<T extends TimestampedReading>(
current: T | null | undefined,
incoming: T | null | undefined
): T | null | undefined {
const currentTs = getTimestampMillis(current?.timestamp);
const incomingTs = getTimestampMillis(incoming?.timestamp);
if (incomingTs === undefined) return current ?? incoming;
if (currentTs === undefined) return incoming ?? current;
return incomingTs >= currentTs ? incoming : current;
}
export interface DevicePageData {
device: Device;
components: Component[];
@ -38,7 +58,7 @@ export default function DevicePage() {
const isMobile = useMobile()
const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
const groupID = useParams<{ groupID: string }>()?.groupID ?? "";
const { state } = useLocation();
const { state, pathname: locationPathname } = useLocation();
const [{ as, team, user }] = useGlobalState()
// console.log(state)
const [device, setDevice] = useState<Device>(state?.device ? Device.create(state.device) : Device.create())
@ -68,6 +88,63 @@ export default function DevicePage() {
const { token } = useHTTP();
const liveContextKeys = useMemo(() => getContextKeys(), [locationPathname]);
const liveContextTypes = useMemo(() => getContextTypes(), [locationPathname]);
/** Matches getDevicePageData: omit ?as= when first context type is "team". */
const liveAs =
as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team")
? as
: undefined;
const liveStatusDeviceIds = useMemo(
() => (deviceID ? [deviceID] : []),
[deviceID]
);
const handleLiveDeviceStatus = useCallback(
(streamDeviceId: string, status: pond.DeviceStatus) => {
setDevice((prev) => {
if (prev.id().toString() !== streamDeviceId) return prev;
const updated = Device.clone(prev);
const incomingStatus = pond.DeviceStatus.fromObject(status);
const currentStatus = prev.status ?? pond.DeviceStatus.create();
incomingStatus.plenum = pickNewestReading(currentStatus.plenum, incomingStatus.plenum);
incomingStatus.sen5x = pickNewestReading(currentStatus.sen5x, incomingStatus.sen5x);
incomingStatus.co = pickNewestReading(currentStatus.co, incomingStatus.co);
incomingStatus.co2 = pickNewestReading(currentStatus.co2, incomingStatus.co2);
incomingStatus.no2 = pickNewestReading(currentStatus.no2, incomingStatus.no2);
incomingStatus.o2 = pickNewestReading(currentStatus.o2, incomingStatus.o2);
incomingStatus.lel = pickNewestReading(currentStatus.lel, incomingStatus.lel);
incomingStatus.h2s = pickNewestReading(currentStatus.h2s, incomingStatus.h2s);
const currentLastActive = getTimestampMillis(currentStatus.lastActive);
const incomingLastActive = getTimestampMillis(incomingStatus.lastActive);
if (
currentLastActive !== undefined &&
incomingLastActive !== undefined &&
incomingLastActive < currentLastActive
) {
incomingStatus.lastActive = currentStatus.lastActive;
}
updated.status = incomingStatus;
return updated;
});
},
[]
);
useDeviceStatusStreams({
deviceIds: liveStatusDeviceIds,
onStatusUpdate: handleLiveDeviceStatus,
token,
keys: liveContextKeys,
types: liveContextTypes,
as: liveAs,
enabled: Boolean(deviceID && token),
});
const loadDevice = () => {
if (loading) return
if (state?.devicePageData) {
@ -173,7 +250,7 @@ export default function DevicePage() {
// When the backend receives a new reading and updates a component,
// this fires and updates the component in state without a page refresh.
useWebSocket<{ key: string; component: Component } | null>({
path: `/live/devices/${deviceID}/components`,
path: `/v1/live/devices/${deviceID}/components`,
parse: (e) => {
try {
const raw = JSON.parse(e.data);
@ -197,32 +274,9 @@ export default function DevicePage() {
return updated;
});
},
token,
enabled: !!deviceID,
});
// --- Real-time device updates (optional) ---
// Updates device-level info (name, status, lastActive, etc.)
useWebSocket<Device | null>({
path: `/live/devices/${deviceID}`,
parse: (e) => {
try {
const raw = JSON.parse(e.data);
const dev = Device.any(raw);
if (!dev?.settings) {
console.warn("[ws] received device without settings:", raw);
return null;
}
return dev;
} catch (err) {
console.warn("[ws] failed to parse device:", err);
return null;
}
},
onMessage: (updatedDevice) => {
if (!updatedDevice) return;
setDevice(updatedDevice);
},
keys: liveContextKeys,
types: liveContextTypes,
as: liveAs,
token,
enabled: !!deviceID,
});

View file

@ -264,10 +264,50 @@ export default function Devices() {
)
}, [])
/** Same context chain as deviceAPI.list / GetContext on the backend (path + group tab + ?as=). */
const statusStreamKeys = useMemo(() => {
if (tab !== "all") {
const g = groups[parseInt(tab)]
if (g) {
const keys = [...getContextKeys()]
keys.push(g.id().toString())
return keys
}
}
return getContextKeys()
}, [tab, groups, location.pathname])
const statusStreamTypes = useMemo(() => {
if (tab !== "all") {
const g = groups[parseInt(tab)]
if (g) {
const types = [...getContextTypes()]
types.push("group")
return types
}
}
return getContextTypes()
}, [tab, groups, location.pathname])
/** Match Device page / getDevicePageData: omit ?as= when URL context already starts with team. */
const statusStreamAs = useMemo(() => {
const types =
tab !== "all" && groups[parseInt(tab)]
? [...getContextTypes(), "group"]
: getContextTypes()
if (as && !(types.length > 0 && types[0] === "team")) {
return as
}
return undefined
}, [as, tab, groups, location.pathname])
useDeviceStatusStreams({
deviceIds,
onStatusUpdate: handleStatusUpdate,
token,
keys: statusStreamKeys,
types: statusStreamTypes,
as: statusStreamAs,
enabled: devices.length > 0 && !!token,
})

View file

@ -32,7 +32,6 @@ import TemperatureIcon from "component/TemperatureIcon";
import FuelIcon from "products/CommonIcons/fuelIcon";
import moment from "moment";
import Warning from "@mui/icons-material/Warning";
import { getTemperatureUnit } from "utils";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useParams } from "react-router-dom";
@ -286,7 +285,7 @@ export default function Heater() {
const tempUnit = () => {
let unit = "°C";
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
unit = "°F";
}
return unit;