Skip to content

Conversation

@nodefather
Copy link

@nodefather nodefather commented Aug 8, 2025

Summary

  • Switch website integration's LiveDataComponent to use SignalR rather than interval polling
  • Join the dashboard update group and refresh data on server events

Testing

  • dotnet build NatureOS.sln (fails: IProactiveMonitoringService not found)
  • npm test in website-integration (fails: missing package.json)

https://chatgpt.com/codex/tasks/task_e_689654e63724832ab03aea8115d85d36

Summary by Sourcery

Replace polling-based live data updates with a SignalR-based connection that joins a dashboard group, fetches initial data, listens for server update events, and manages connection state.

Enhancements:

  • Switch LiveDataComponent to use SignalR for real-time updates instead of interval polling
  • Join the dashboard update group and refresh data on incoming server events
  • Add connection lifecycle handling including automatic reconnect, initial data fetch, error state, and cleanup on unmount

@sourcery-ai
Copy link

sourcery-ai bot commented Aug 8, 2025

Reviewer's Guide

This PR replaces the polling-based data refresh in LiveDataComponent with a SignalR-driven real-time update flow by establishing a hub connection, joining the dashboard group, and subscribing to server push events for immediate data refresh.

Sequence diagram for SignalR-based live data updates in LiveDataComponent

sequenceDiagram
    participant User as actor User
    participant LiveDataComponent
    participant SignalRHub as NatureOS SignalR Hub

    User->>LiveDataComponent: Mounts component
    LiveDataComponent->>SignalRHub: Establish SignalR connection
    LiveDataComponent->>SignalRHub: Invoke JoinDashboardGroup
    SignalRHub-->>LiveDataComponent: Connection established
    LiveDataComponent->>SignalRHub: Fetch initial data
    SignalRHub-->>LiveDataComponent: Initial data response
    SignalRHub-->>LiveDataComponent: DeviceUpdate/SimulationUpdate/SystemUpdate events
    LiveDataComponent->>SignalRHub: Fetch updated data (on event)
    SignalRHub-->>LiveDataComponent: Updated data response
    User-->>LiveDataComponent: Unmounts component
    LiveDataComponent->>SignalRHub: Stop connection
Loading

File-Level Changes

Change Details Files
Switch from interval polling to SignalR-based real-time updates
  • Imported HubConnectionBuilder and LogLevel from '@microsoft/signalr'
  • Configured and built a HubConnection with automatic reconnect and logging
  • Established the connection and handled startup errors
  • Joined the dashboard group via 'JoinDashboardGroup' invocation
  • Removed setInterval-based polling and standalone initial fetch
  • Subscribed to 'DeviceUpdate', 'SimulationUpdate', and 'SystemUpdate' events to call fetchLiveData
  • Handled connection close events to update connection state
  • Stopped the SignalR connection on component unmount
website-integration/components/LiveDataComponent.jsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @nodefather - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `website-integration/components/LiveDataComponent.jsx:10` </location>
<code_context>
 import { motion, AnimatePresence } from 'framer-motion';
+import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

 export default function LiveDataComponent() {
   const [liveData, setLiveData] = useState(null);
</code_context>

<issue_to_address>
Consider extracting the SignalR and polling logic into custom hooks to keep the component focused on rendering.

```suggestion
This component has grown a lot of nested logic around SignalR and polling. Extract both into small custom hooks to keep the component focused on rendering.

1) Create a `useInterval` hook for polling:
```ts
// hooks/useInterval.ts
import { useEffect } from 'react';
export function useInterval(cb: () => void, delay: number) {
  useEffect(() => {
    if (delay == null) return;
    const id = setInterval(cb, delay);
    return () => clearInterval(id);
  }, [cb, delay]);
}
```

2) Create a `useSignalR` hook to handle connection, reconnection, events & errors:
```ts
// hooks/useSignalR.ts
import { useEffect, useState } from 'react';
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

const EVENTS = ['DeviceUpdate', 'SimulationUpdate', 'SystemUpdate'];

export function useSignalR(onUpdate: () => void) {
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const conn = new HubConnectionBuilder()
       .withUrl('/natureos-hub')
       .withAutomaticReconnect()
       .configureLogging(LogLevel.Information)
       .build();

    conn.onclose(() => setIsConnected(false));
    EVENTS.forEach(e => conn.on(e, onUpdate));

    (async () => {
      try {
        await conn.start();
        setIsConnected(true);
        await conn.invoke('JoinDashboardGroup');
        await onUpdate();
      } catch (err: any) {
        setError(err.message);
        setIsConnected(false);
      }
    })();

    return () => { conn.stop(); };
  }, [onUpdate]);

  return { isConnected, error };
}
```

3) Simplify your component to just:
```ts
// LiveDataComponent.tsx
import { useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import { useInterval } from './hooks/useInterval';
import { useSignalR } from './hooks/useSignalR';

export default function LiveDataComponent() {
  const [data, setData] = useState<any>(null);
  const [lastUpdate, setLastUpdate] = useState<Date>();
  const fetchLiveData = useCallback(async () => {
    try {
      const d = await fetch('/natureos').then(r => r.json());
      setData(d);
      setLastUpdate(new Date());
    } catch {
      /* handle error locally or via useSignalR */
    }
  }, []);

  const { isConnected, error } = useSignalR(fetchLiveData);
  useInterval(fetchLiveData, 5000);

  if (!data) {
    return (
      <div className="flex items-center justify-center p-8">
        <motion.div
          animate={{ rotate: 360 }}
          transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
          className="w-8 h-8 border-2 border-green-500 border-t-transparent rounded-full"
        />
      </div>
    );
  }
  // ...render data, connection state, lastUpdate, error...
}
```

This preserves all functionality but pulls complex logic into reusable hooks, improving readability and testability.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

import { motion, AnimatePresence } from 'framer-motion';
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

export default function LiveDataComponent() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting the SignalR and polling logic into custom hooks to keep the component focused on rendering.

Suggested change
export default function LiveDataComponent() {
This component has grown a lot of nested logic around SignalR and polling. Extract both into small custom hooks to keep the component focused on rendering.
1) Create a `useInterval` hook for polling:
```ts
// hooks/useInterval.ts
import { useEffect } from 'react';
export function useInterval(cb: () => void, delay: number) {
useEffect(() => {
if (delay == null) return;
const id = setInterval(cb, delay);
return () => clearInterval(id);
}, [cb, delay]);
}
  1. Create a useSignalR hook to handle connection, reconnection, events & errors:
// hooks/useSignalR.ts
import { useEffect, useState } from 'react';
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

const EVENTS = ['DeviceUpdate', 'SimulationUpdate', 'SystemUpdate'];

export function useSignalR(onUpdate: () => void) {
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const conn = new HubConnectionBuilder()
       .withUrl('/natureos-hub')
       .withAutomaticReconnect()
       .configureLogging(LogLevel.Information)
       .build();

    conn.onclose(() => setIsConnected(false));
    EVENTS.forEach(e => conn.on(e, onUpdate));

    (async () => {
      try {
        await conn.start();
        setIsConnected(true);
        await conn.invoke('JoinDashboardGroup');
        await onUpdate();
      } catch (err: any) {
        setError(err.message);
        setIsConnected(false);
      }
    })();

    return () => { conn.stop(); };
  }, [onUpdate]);

  return { isConnected, error };
}
  1. Simplify your component to just:
// LiveDataComponent.tsx
import { useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import { useInterval } from './hooks/useInterval';
import { useSignalR } from './hooks/useSignalR';

export default function LiveDataComponent() {
  const [data, setData] = useState<any>(null);
  const [lastUpdate, setLastUpdate] = useState<Date>();
  const fetchLiveData = useCallback(async () => {
    try {
      const d = await fetch('/natureos').then(r => r.json());
      setData(d);
      setLastUpdate(new Date());
    } catch {
      /* handle error locally or via useSignalR */
    }
  }, []);

  const { isConnected, error } = useSignalR(fetchLiveData);
  useInterval(fetchLiveData, 5000);

  if (!data) {
    return (
      <div className="flex items-center justify-center p-8">
        <motion.div
          animate={{ rotate: 360 }}
          transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
          className="w-8 h-8 border-2 border-green-500 border-t-transparent rounded-full"
        />
      </div>
    );
  }
  // ...render data, connection state, lastUpdate, error...
}

This preserves all functionality but pulls complex logic into reusable hooks, improving readability and testability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants