Răsfoiți Sursa

feat(ui): add update notification indicator and version link

- Add download icon with blue pulse dot in header when updates are available
- Make "Latest Version" in Settings a clickable link to GitHub releases
- Revert VERSION to 3.5.0 for pre-merge state
- Update docker-compose.yml image tags from :feature-react-ui to :main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 2 zile în urmă
părinte
comite
714672344e

+ 1 - 1
VERSION

@@ -1 +1 @@
-4.0.0
+3.5.0

+ 2 - 3
docker-compose.yml

@@ -1,10 +1,9 @@
-# TODO: Revert image tags from feature-react-ui to main before merging to main branch
 services:
   frontend:
     build:
       context: ./frontend
       dockerfile: Dockerfile
-    image: ghcr.io/tuanchris/dune-weaver-frontend:feature-react-ui
+    image: ghcr.io/tuanchris/dune-weaver-frontend:main
     restart: always
     cap_add:
       - SYS_NICE  # Enable real-time thread priority for smooth UART communication
@@ -19,7 +18,7 @@ services:
 
   backend:
     build: .
-    image: ghcr.io/tuanchris/dune-weaver:feature-react-ui
+    image: ghcr.io/tuanchris/dune-weaver:main
     restart: always
     # Pin motion-critical backend to cores 0-2 (Raspberry Pi 4/5 has cores 0-3)
     # This prevents CPU contention from touch app blocking I/O calls

+ 30 - 0
frontend/src/components/layout/Layout.tsx

@@ -70,6 +70,9 @@ export function Layout() {
   const [sensorHomingFailed, setSensorHomingFailed] = useState(false)
   const [isRecoveringHoming, setIsRecoveringHoming] = useState(false)
 
+  // Update availability
+  const [updateAvailable, setUpdateAvailable] = useState(false)
+
   // Fetch app settings
   const fetchAppSettings = () => {
     apiClient.get<{ app?: { name?: string; custom_logo?: string } }>('/api/settings')
@@ -99,6 +102,17 @@ export function Layout() {
     // Refetch when active table changes
   }, [activeTable?.id])
 
+  // Check for software updates on mount
+  useEffect(() => {
+    apiClient.get<{ update_available?: boolean }>('/api/version')
+      .then((data) => {
+        if (data.update_available) {
+          setUpdateAvailable(true)
+        }
+      })
+      .catch(() => {})
+  }, [activeTable?.id])
+
   // Homing completion countdown timer
   useEffect(() => {
     if (!homingJustCompleted || keepHomingLogsOpen) return
@@ -1557,6 +1571,14 @@ export function Layout() {
 
           {/* Desktop actions */}
           <div className="hidden md:flex items-center gap-0 ml-2">
+            {updateAvailable && (
+              <Link to="/settings?section=version" title="Software update available">
+                <span className="relative flex items-center justify-center w-8 h-8 rounded-full hover:bg-accent transition-colors">
+                  <span className="material-icons-outlined text-xl">download</span>
+                  <span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
+                </span>
+              </Link>
+            )}
             <Popover>
               <PopoverTrigger asChild>
                 <Button
@@ -1608,6 +1630,14 @@ export function Layout() {
 
           {/* Mobile actions */}
           <div className="flex md:hidden items-center gap-0 ml-2">
+            {updateAvailable && (
+              <Link to="/settings?section=version" title="Software update available">
+                <span className="relative flex items-center justify-center w-8 h-8 rounded-full hover:bg-accent transition-colors">
+                  <span className="material-icons-outlined text-xl">download</span>
+                  <span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
+                </span>
+              </Link>
+            )}
             <Popover open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
               <PopoverTrigger asChild>
                 <Button

+ 10 - 1
frontend/src/pages/SettingsPage.tsx

@@ -2208,7 +2208,16 @@ export function SettingsPage() {
               <div className="flex-1">
                 <p className="font-medium">Latest Version</p>
                 <p className={`text-sm ${versionInfo?.update_available ? 'text-green-600 dark:text-green-400 font-medium' : 'text-muted-foreground'}`}>
-                  {versionInfo?.latest ? `v${versionInfo.latest}` : 'Checking...'}
+                  {versionInfo?.latest ? (
+                    <a
+                      href={`https://github.com/tuanchris/dune-weaver/releases/tag/v${versionInfo.latest}`}
+                      target="_blank"
+                      rel="noopener noreferrer"
+                      className="underline underline-offset-2 hover:opacity-80 transition-opacity"
+                    >
+                      v{versionInfo.latest}
+                    </a>
+                  ) : 'Checking...'}
                   {versionInfo?.update_available && ' (Update available!)'}
                 </p>
               </div>