ソースを参照

feat: implement comprehensive versioning system

- Add git-based version detection using tags and commit hashes
- Support semantic versioning with development/release builds
- Implement /api/version endpoint with detailed version information
- Add version information to server startup logs
- Update web UI version checker with enhanced API integration
- Generate version.json during build for web UI consumption
- Display version badge in web UI footer
- Handle dirty working directory detection
- Include build timestamp and branch information

CMake versioning features:
- Git tag detection for release builds
- Git describe fallback for development builds
- Automatic version header generation
- Clean/dirty status detection
- Build timestamp tracking

API enhancements:
- New /api/version public endpoint
- Enhanced VersionInfo interface
- Structured response with commit, branch, and build info
- Version integration in health endpoint

Web UI improvements:
- Real-time version checking against server API
- Version badge display with dirty indicator
- Update notification when versions diverge
- Automatic refresh capability

Resolves: #48
Fszontagh 3 ヶ月 前
コミット
5dc7608fee

+ 134 - 19
CMakeLists.txt

@@ -23,6 +23,137 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 # Include dependencies
 include(FindDependencies)
 
+# Version detection from git
+find_package(Git QUIET)
+if(GIT_FOUND)
+    # Get current git commit hash (short)
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_COMMIT_HASH
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    # Get full git commit hash
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    # Get current git tag (prefer annotated tags)
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match --abbrev=0
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_TAG
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    # Get git describe for fallback (e.g., v1.0.0-5-g12345678)
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} describe --tags --always --abbrev=8
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_DESCRIBE
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    # Get git branch
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_BRANCH
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    # Check if working directory is clean
+    execute_process(
+        COMMAND ${GIT_EXECUTABLE} status --porcelain
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE GIT_STATUS
+        OUTPUT_STRIP_TRAILING_WHITESPACE
+        ERROR_QUIET
+    )
+    
+    if(NOT GIT_COMMIT_HASH)
+        set(GIT_COMMIT_HASH "unknown")
+    endif()
+    if(NOT GIT_COMMIT_HASH_FULL)
+        set(GIT_COMMIT_HASH_FULL "unknown")
+    endif()
+    if(NOT GIT_DESCRIBE)
+        set(GIT_DESCRIBE "unknown")
+    endif()
+    if(NOT GIT_BRANCH)
+        set(GIT_BRANCH "unknown")
+    endif()
+    
+    # Determine if working directory is clean
+    if(GIT_STATUS STREQUAL "")
+        set(GIT_CLEAN "true")
+    else()
+        set(GIT_CLEAN "false")
+    endif()
+else()
+    set(GIT_COMMIT_HASH "unknown")
+    set(GIT_COMMIT_HASH_FULL "unknown")
+    set(GIT_TAG "")
+    set(GIT_DESCRIBE "unknown")
+    set(GIT_BRANCH "unknown")
+    set(GIT_CLEAN "false")
+endif()
+
+# Determine version string
+if(GIT_TAG)
+    # Release build with tag
+    set(PROJECT_VERSION_FULL "${GIT_TAG}")
+    set(PROJECT_VERSION_TYPE "release")
+elseif(GIT_DESCRIBE AND NOT GIT_DESCRIBE STREQUAL "unknown")
+    # Development build with git describe
+    set(PROJECT_VERSION_FULL "${GIT_DESCRIBE}")
+    set(PROJECT_VERSION_TYPE "development")
+else()
+    # Fallback to commit hash
+    set(PROJECT_VERSION_FULL "g${GIT_COMMIT_HASH}")
+    set(PROJECT_VERSION_TYPE "development")
+endif()
+
+# Add dirty suffix if working directory is not clean
+if(GIT_CLEAN STREQUAL "false")
+    set(PROJECT_VERSION_FULL "${PROJECT_VERSION_FULL}-dirty")
+endif()
+
+# Build timestamp
+string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%dT%H:%M:%SZ" UTC)
+
+# Set project variables
+set(PROJECT_VERSION_COMMIT "${GIT_COMMIT_HASH}")
+set(PROJECT_VERSION_COMMIT_FULL "${GIT_COMMIT_HASH_FULL}")
+set(PROJECT_VERSION_BRANCH "${GIT_BRANCH}")
+set(PROJECT_VERSION_CLEAN "${GIT_CLEAN}")
+set(PROJECT_VERSION_BUILD_TIME "${BUILD_TIMESTAMP}")
+
+# Display version information
+message(STATUS "Version Information:")
+message(STATUS "  Version: ${PROJECT_VERSION_FULL}")
+message(STATUS "  Type: ${PROJECT_VERSION_TYPE}")
+message(STATUS "  Commit: ${GIT_COMMIT_HASH}")
+message(STATUS "  Branch: ${GIT_BRANCH}")
+message(STATUS "  Clean: ${GIT_CLEAN}")
+message(STATUS "  Build Time: ${BUILD_TIMESTAMP}")
+
+# Generate version header file
+configure_file(
+    "${CMAKE_CURRENT_SOURCE_DIR}/include/version.h.in"
+    "${CMAKE_CURRENT_BINARY_DIR}/include/version.h"
+    @ONLY
+)
+
 # Option for CUDA support
 option(SD_CUDA_SUPPORT "Enable CUDA support in stable-diffusion.cpp" ON)
 
@@ -85,23 +216,7 @@ if(BUILD_WEBUI)
         set(WEBUI_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/webui)
         set(WEBUI_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/webui)
 
-        # Get git commit hash for versioning
-        find_package(Git QUIET)
-        if(GIT_FOUND)
-            execute_process(
-                COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD
-                WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-                OUTPUT_VARIABLE GIT_COMMIT_HASH
-                OUTPUT_STRIP_TRAILING_WHITESPACE
-                ERROR_QUIET
-            )
-            if(NOT GIT_COMMIT_HASH)
-                set(GIT_COMMIT_HASH "unknown")
-            endif()
-        else()
-            set(GIT_COMMIT_HASH "unknown")
-        endif()
-        message(STATUS "Git commit hash for UI versioning: ${GIT_COMMIT_HASH}")
+        message(STATUS "Git commit hash for UI versioning: ${PROJECT_VERSION_FULL}")
 
         # Get build timestamp
         string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%dT%H:%M:%SZ" UTC)
@@ -113,12 +228,12 @@ if(BUILD_WEBUI)
             COMMAND ${CMAKE_COMMAND} -E remove_directory ${WEBUI_OUTPUT_DIR}
             COMMAND ${CMAKE_COMMAND} -E make_directory ${WEBUI_OUTPUT_DIR}
             # Generate version file
-            COMMAND ${CMAKE_COMMAND} -E echo "{\"version\":\"${GIT_COMMIT_HASH}\",\"buildTime\":\"${BUILD_TIMESTAMP}\"}" > ${WEBUI_SOURCE_DIR}/public/version.json
+            COMMAND ${CMAKE_COMMAND} -E echo "{\"version\":\"${PROJECT_VERSION_FULL}\",\"commit\":\"${PROJECT_VERSION_COMMIT}\",\"branch\":\"${PROJECT_VERSION_BRANCH}\",\"buildTime\":\"${BUILD_TIMESTAMP}\",\"type\":\"${PROJECT_VERSION_TYPE}\",\"clean\":${PROJECT_VERSION_CLEAN}}" > ${WEBUI_SOURCE_DIR}/public/version.json
             COMMAND ${NPM_EXECUTABLE} install
             COMMAND ${NPM_EXECUTABLE} run build
             COMMAND ${CMAKE_COMMAND} -E copy_directory ${WEBUI_SOURCE_DIR}/out ${WEBUI_OUTPUT_DIR}
             WORKING_DIRECTORY ${WEBUI_SOURCE_DIR}
-            COMMENT "Building Web UI with npm (version: ${GIT_COMMIT_HASH})"
+            COMMENT "Building Web UI with npm (version: ${PROJECT_VERSION_FULL})"
             VERBATIM
         )
 

+ 5 - 0
include/server.h

@@ -107,6 +107,11 @@ private:
      */
     void handleApiStatus(const httplib::Request& req, httplib::Response& res);
 
+    /**
+     * @brief Version information endpoint handler
+     */
+    void handleVersion(const httplib::Request& req, httplib::Response& res);
+
     /**
      * @brief Models list endpoint handler
      */

+ 37 - 0
include/version.h.in

@@ -0,0 +1,37 @@
+#pragma once
+
+#include <string>
+
+#define PROJECT_VERSION_FULL "@PROJECT_VERSION_FULL@"
+#define PROJECT_VERSION_TYPE "@PROJECT_VERSION_TYPE@"
+#define PROJECT_VERSION_COMMIT "@PROJECT_VERSION_COMMIT@"
+#define PROJECT_VERSION_COMMIT_FULL "@PROJECT_VERSION_COMMIT_FULL@"
+#define PROJECT_VERSION_BRANCH "@PROJECT_VERSION_BRANCH@"
+#define PROJECT_VERSION_CLEAN "@PROJECT_VERSION_CLEAN@"
+#define PROJECT_VERSION_BUILD_TIME "@PROJECT_VERSION_BUILD_TIME@"
+
+namespace sd_rest {
+
+struct VersionInfo {
+    std::string version_full;
+    std::string version_type;
+    std::string commit_short;
+    std::string commit_full;
+    std::string branch;
+    bool is_clean;
+    std::string build_time;
+    
+    VersionInfo() :
+        version_full(PROJECT_VERSION_FULL),
+        version_type(PROJECT_VERSION_TYPE),
+        commit_short(PROJECT_VERSION_COMMIT),
+        commit_full(PROJECT_VERSION_COMMIT_FULL),
+        branch(PROJECT_VERSION_BRANCH),
+        is_clean(PROJECT_VERSION_CLEAN == "true"),
+        build_time(PROJECT_VERSION_BUILD_TIME) {}
+};
+
+// Global version info instance
+extern const VersionInfo VERSION_INFO;
+
+} // namespace sd_rest

+ 3 - 42
src/CMakeLists.txt

@@ -12,6 +12,7 @@ set(SOURCES
     jwt_auth.cpp
     user_manager.cpp
     auth_middleware.cpp
+    version.cpp
 )
 
 # Collect header files
@@ -25,6 +26,7 @@ set(HEADERS
     ../include/user_manager.h
     ../include/auth_middleware.h
     ../include/server_config.h
+    ${CMAKE_CURRENT_BINARY_DIR}/../include/version.h
 )
 
 # Add PAM authentication files if enabled
@@ -49,6 +51,7 @@ set_target_properties(stable-diffusion-rest-server PROPERTIES
 # Add include directories
 target_include_directories(stable-diffusion-rest-server PRIVATE
     ${CMAKE_CURRENT_SOURCE_DIR}/../include
+    ${CMAKE_CURRENT_BINARY_DIR}/../include
 )
 
 # Link against dependencies
@@ -98,48 +101,6 @@ install(FILES ${HEADERS}
     DESTINATION include/stable-diffusion-rest
 )
 
-# Add the test for model detection
-if (CMAKE_BUILD_TYPE STREQUAL "Debug")
-    add_executable(test_model_detection
-        ../test_model_detection.cpp
-        model_detector.cpp
-        stable_diffusion_wrapper.cpp
-        model_manager.cpp
-        generation_queue.cpp
-        logger.cpp
-    )
-endif()
-# Set target properties
-set_target_properties(test_model_detection PROPERTIES
-    CXX_STANDARD 17
-    CXX_STANDARD_REQUIRED ON
-    CXX_EXTENSIONS OFF
-)
-
-# Add include directories
-target_include_directories(test_model_detection PRIVATE
-    ${CMAKE_CURRENT_SOURCE_DIR}/../include
-)
-
-# Link against dependencies - same as main server executable
-target_link_libraries(test_model_detection PRIVATE
-    sd-cpp
-    ggml
-    ggml-base
-    ggml-cpu
-    ${DEPENDENCY_LIBRARIES}
-    OpenMP::OpenMP_CXX
-    nlohmann_json::nlohmann_json
-    pthread
-    ${CMAKE_DL_LIBS}
-)
-
-# Add Apple frameworks if needed
-if(APPLE)
-    target_link_libraries(test_model_detection PRIVATE "-framework Foundation" "-framework Metal" "-framework MetalKit")
-endif()
 
-# Add dependency on sd-cpp target to ensure correct build order
-add_dependencies(test_model_detection sd-cpp)
 
 message(STATUS "Configured stable-diffusion-rest-server executable")

+ 8 - 0
src/main.cpp

@@ -13,6 +13,7 @@
 #include "server.h"
 #include "server_config.h"
 #include "user_manager.h"
+#include "version.h"
 
 // Global flag for signal handling
 std::atomic<bool> g_running(true);
@@ -336,6 +337,9 @@ int main(int argc, char* argv[]) {
     }
 
     LOG_INFO("=== Stable Diffusion REST Server Starting ===");
+    LOG_INFO("Version: " + sd_rest::VERSION_INFO.version_full + " (" + sd_rest::VERSION_INFO.version_type + ")");
+    LOG_INFO("Commit: " + sd_rest::VERSION_INFO.commit_short + (sd_rest::VERSION_INFO.is_clean ? "" : " (dirty)"));
+    LOG_INFO("Build time: " + sd_rest::VERSION_INFO.build_time);
     if (config.enableFileLogging) {
         LOG_INFO("File logging enabled: " + config.logFilePath);
     }
@@ -350,6 +354,10 @@ int main(int argc, char* argv[]) {
 
     if (config.verbose) {
         std::cout << "\n=== Configuration ===" << std::endl;
+        std::cout << "Version: " << sd_rest::VERSION_INFO.version_full << " (" << sd_rest::VERSION_INFO.version_type << ")" << std::endl;
+        std::cout << "Commit: " << sd_rest::VERSION_INFO.commit_short << (sd_rest::VERSION_INFO.is_clean ? "" : " (dirty)") << std::endl;
+        std::cout << "Build time: " << sd_rest::VERSION_INFO.build_time << std::endl;
+        std::cout << std::endl;
         std::cout << "Server:" << std::endl;
         std::cout << "  Host: " << config.host << std::endl;
         std::cout << "  Port: " << config.port << std::endl;

+ 26 - 1
src/server.cpp

@@ -5,6 +5,7 @@
 #include "utils.h"
 #include "auth_middleware.h"
 #include "user_manager.h"
+#include "version.h"
 #include <httplib.h>
 #include <nlohmann/json.hpp>
 #include <iostream>
@@ -163,6 +164,11 @@ void Server::registerEndpoints() {
         handleApiStatus(req, res);
     });
 
+    // Version information endpoint (public)
+    m_httpServer->Get("/api/version", [this](const httplib::Request& req, httplib::Response& res) {
+        handleVersion(req, res);
+    });
+
     // Apply authentication middleware to protected endpoints
     auto withAuth = [this](std::function<void(const httplib::Request&, httplib::Response&)> handler) {
         return [this, handler](const httplib::Request& req, httplib::Response& res) {
@@ -919,7 +925,7 @@ void Server::handleHealthCheck(const httplib::Request& /*req*/, httplib::Respons
             {"status", "healthy"},
             {"timestamp", std::chrono::duration_cast<std::chrono::seconds>(
                 std::chrono::system_clock::now().time_since_epoch()).count()},
-            {"version", "1.0.0"}
+            {"version", sd_rest::VERSION_INFO.version_full}
         };
         sendJsonResponse(res, response);
     } catch (const std::exception& e) {
@@ -951,6 +957,25 @@ void Server::handleApiStatus(const httplib::Request& /*req*/, httplib::Response&
     }
 }
 
+void Server::handleVersion(const httplib::Request& /*req*/, httplib::Response& res) {
+    try {
+        nlohmann::json response = {
+            {"version", sd_rest::VERSION_INFO.version_full},
+            {"type", sd_rest::VERSION_INFO.version_type},
+            {"commit", {
+                {"short", sd_rest::VERSION_INFO.commit_short},
+                {"full", sd_rest::VERSION_INFO.commit_full}
+            }},
+            {"branch", sd_rest::VERSION_INFO.branch},
+            {"clean", sd_rest::VERSION_INFO.is_clean},
+            {"build_time", sd_rest::VERSION_INFO.build_time}
+        };
+        sendJsonResponse(res, response);
+    } catch (const std::exception& e) {
+        sendErrorResponse(res, std::string("Version check failed: ") + e.what(), 500);
+    }
+}
+
 // Helper function to convert ModelDetails vector to JSON array
 nlohmann::json Server::modelDetailsToJson(const std::vector<ModelManager::ModelDetails>& modelDetails) {
     nlohmann::json jsonArray = nlohmann::json::array();

+ 7 - 0
src/version.cpp

@@ -0,0 +1,7 @@
+#include "version.h"
+
+namespace sd_rest {
+
+const VersionInfo VERSION_INFO;
+
+} // namespace sd_rest

+ 1 - 3
webui/app/layout.tsx

@@ -1,8 +1,7 @@
 import type { Metadata } from "next";
 import { Inter } from "next/font/google";
 import "./globals.css";
-import { ThemeProvider } from "@/components/theme-provider";
-import { VersionChecker } from "@/components/version-checker";
+import { ThemeProvider } from "@/components/layout";
 import { AuthProvider } from "@/lib/auth-context";
 import { ModelSelectionProvider } from "@/contexts/model-selection-context";
 
@@ -37,7 +36,6 @@ export default function RootLayout({
         >
           <AuthProvider>
             <ModelSelectionProvider>
-              <VersionChecker />
               {children}
             </ModelSelectionProvider>
           </AuthProvider>

+ 15 - 3
webui/components/layout/app-layout.tsx

@@ -1,8 +1,9 @@
 'use client';
 
 import { ReactNode } from 'react';
-import { Sidebar } from '../sidebar';
-import { ModelStatusBar } from '../model-status-bar';
+import { Sidebar } from './sidebar';
+import { ModelStatusBar } from '../features/models';
+import { VersionChecker } from './version-checker';
 import { LayoutProvider } from './layout-context';
 
 /**
@@ -50,8 +51,19 @@ export function AppLayout({ children }: AppLayoutProps) {
         <footer style={{
           gridArea: 'footer',
           minHeight: '3rem',
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          padding: '0 1rem',
+          borderTop: '1px solid var(--color-border)',
+          backgroundColor: 'var(--color-card)',
         }}>
-          <ModelStatusBar />
+          <div style={{ flex: 1 }}>
+            <ModelStatusBar />
+          </div>
+          <div style={{ flex: 'none', marginLeft: '1rem' }}>
+            <VersionChecker />
+          </div>
         </footer>
       </div>
     </LayoutProvider>

+ 112 - 0
webui/components/layout/version-checker.tsx

@@ -0,0 +1,112 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { AlertCircle, RefreshCw, Info } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { getVersion, VersionInfo } from '@/lib/api';
+
+export function VersionChecker() {
+  const [currentVersion, setCurrentVersion] = useState<string | null>(null);
+  const [serverVersion, setServerVersion] = useState<VersionInfo | null>(null);
+  const [showUpdate, setShowUpdate] = useState(false);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    // Get initial version from version.json
+    const loadCurrentVersion = async () => {
+      try {
+        const response = await fetch('/ui/version.json', {
+          cache: 'no-store' // Always fetch fresh version
+        });
+        if (response.ok) {
+          const data = await response.json();
+          setCurrentVersion(data.version);
+        }
+      } catch (error) {
+        console.warn('Failed to load UI version:', error);
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    loadCurrentVersion();
+  }, []);
+
+  useEffect(() => {
+    // Check server version periodically (every 5 minutes)
+    const checkVersion = async () => {
+      try {
+        const versionInfo = await getVersion();
+        setServerVersion(versionInfo);
+
+        // If we have both versions and they don't match, show update notification
+        if (currentVersion && versionInfo.version !== currentVersion) {
+          setShowUpdate(true);
+        }
+      } catch (error) {
+        console.warn('Failed to check server version:', error);
+      }
+    };
+
+    // Initial check after 2 seconds
+    const initialTimeout = setTimeout(checkVersion, 2000);
+
+    // Periodic check every 5 minutes
+    const interval = setInterval(checkVersion, 5 * 60 * 1000);
+
+    return () => {
+      clearTimeout(initialTimeout);
+      clearInterval(interval);
+    };
+  }, [currentVersion]);
+
+  const handleRefresh = () => {
+    // Force reload to get new version
+    window.location.reload();
+  };
+
+  if (loading || !serverVersion) {
+    return null;
+  }
+
+  // Version badge display (always visible in footer/header)
+  const VersionBadge = () => (
+    <div className="flex items-center gap-2 text-xs text-muted-foreground">
+      <Info className="h-3 w-3" />
+      <span>v{serverVersion.version}</span>
+      {!serverVersion.clean && <span className="text-amber-500">*</span>}
+    </div>
+  );
+
+  // Update notification overlay
+  if (showUpdate) {
+    return (
+      <>
+        <VersionBadge />
+        <div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 animate-in slide-in-from-top duration-300">
+          <div className="bg-amber-500 dark:bg-amber-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 max-w-md">
+            <AlertCircle className="h-5 w-5 flex-shrink-0" />
+            <div className="flex-1">
+              <p className="font-semibold">New UI Version Available</p>
+              <p className="text-sm opacity-90">
+                A new version of UI has been deployed. Current: {currentVersion}, Server: {serverVersion.version}. 
+                Refresh to get latest updates.
+              </p>
+            </div>
+            <Button
+              size="sm"
+              variant="secondary"
+              onClick={handleRefresh}
+              className="flex-shrink-0"
+            >
+              <RefreshCw className="h-4 w-4 mr-1" />
+              Refresh
+            </Button>
+          </div>
+        </div>
+      </>
+    );
+  }
+
+  return <VersionBadge />;
+}

+ 23 - 0
webui/lib/api.ts

@@ -247,6 +247,18 @@ export interface HealthStatus {
   version?: string;
 }
 
+export interface VersionInfo {
+  version: string;
+  type: string;
+  commit: {
+    short: string;
+    full: string;
+  };
+  branch: string;
+  clean: boolean;
+  build_time: string;
+}
+
 class ApiClient {
   private baseUrl: string = '';
   private isInitialized: boolean = false;
@@ -1089,4 +1101,15 @@ export const authApi = {
   }
 };
 
+// Version API
+export async function getVersion(): Promise<VersionInfo> {
+  const response = await apiRequest('/version');
+  
+  if (!response.ok) {
+    throw new Error('Failed to get version information');
+  }
+  
+  return response.json();
+}
+
 export const apiClient = new ApiClient();