diff --git a/api/routes/config.py b/api/routes/config.py index 7213ac6..ea1b567 100644 --- a/api/routes/config.py +++ b/api/routes/config.py @@ -1,9 +1,10 @@ +import httpx +import time from fastapi import APIRouter, Depends, Form from typing import Annotated from services.config import ConfigCenter from services.auth import get_current_active_user, User, has_users from api.response import success - router = APIRouter(prefix="/api/config", tags=["config"]) @@ -37,9 +38,41 @@ async def get_all_config( @router.get("/status") async def get_system_status(): system_info = { - "version": "1.0.0", - "title": await ConfigCenter.get("APP_NAME", "Foxel"), + "version": "v1.0.0", + "title": await ConfigCenter.get("APP_NAME", "Foxel"), "logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"), "is_initialized": await has_users() } return success(system_info) + + +latest_version_cache = { + "timestamp": 0, + "data": None +} + + +@router.get("/latest-version") +async def get_latest_version(): + current_time = time.time() + if current_time - latest_version_cache["timestamp"] < 3600 and latest_version_cache["data"]: + return success(latest_version_cache["data"]) + try: + async with httpx.AsyncClient(timeout=10.0, proxy="http://127.0.0.1:7897") as client: + resp = await client.get( + "https://api.github.com/repos/DrizzleTime/Foxel/releases/latest", + follow_redirects=True, + ) + resp.raise_for_status() + data = resp.json() + version_info = { + "latest_version": data.get("tag_name"), + "body": data.get("body") + } + latest_version_cache["timestamp"] = current_time + latest_version_cache["data"] = version_info + return success(version_info) + except httpx.RequestError as e: + if latest_version_cache["data"]: + return success(latest_version_cache["data"]) + return success({"latest_version": None, "body": None}) diff --git a/schemas/tasks.py b/schemas/tasks.py index 52c64b9..418c417 100644 --- a/schemas/tasks.py +++ b/schemas/tasks.py @@ -28,4 +28,4 @@ class AutomationTaskRead(AutomationTaskBase): id: int class Config: - orm_mode = True + from_attributes = True diff --git a/services/middleware/logging_middleware.py b/services/middleware/logging_middleware.py index 33f853b..befa6b3 100644 --- a/services/middleware/logging_middleware.py +++ b/services/middleware/logging_middleware.py @@ -17,7 +17,7 @@ class LoggingMiddleware(BaseHTTPMiddleware): try: if token_str and token_str.startswith("Bearer "): token = token_str.split(" ")[1] - payload = jwt.decode(token, ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM]) + payload = jwt.decode(token, await ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM]) username = payload.get("sub") if username: user_account = await UserAccount.get_or_none(username=username) diff --git a/web/bun.lock b/web/bun.lock index 5ed3b32..a33d531 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -11,6 +11,7 @@ "date-fns": "^4.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-markdown": "^10.1.0", "react-router": "^7.8.0", }, "devDependencies": { @@ -751,7 +752,7 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], @@ -899,6 +900,8 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@uiw/react-markdown-preview/react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="], + "@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/web/package.json b/web/package.json index f9e1382..c93f3a3 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,7 @@ "date-fns": "^4.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-markdown": "^10.1.0", "react-router": "^7.8.0" }, "devDependencies": { diff --git a/web/src/api/config.ts b/web/src/api/config.ts index c49d632..e428a6f 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -25,3 +25,7 @@ export interface SystemStatus { export async function status() { return request('/config/status'); } + +export async function getLatestVersion() { + return request<{ latest_version: string | null, body: string | null }>('/config/latest-version'); +} diff --git a/web/src/layout/SideNav.tsx b/web/src/layout/SideNav.tsx index 524c3c8..7d1abc8 100644 --- a/web/src/layout/SideNav.tsx +++ b/web/src/layout/SideNav.tsx @@ -1,11 +1,19 @@ -import { Layout, Menu, theme, Button, Modal } from 'antd'; +import { Layout, Menu, theme, Button, Modal, Tag, Tooltip } from 'antd'; import { navGroups } from './nav.ts'; import type { NavItem, NavGroup } from './nav.ts'; -import { memo, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { useSystemStatus } from '../contexts/SystemContext.tsx'; -import { GithubOutlined, MenuFoldOutlined, SendOutlined, WechatOutlined } from '@ant-design/icons'; +import { + CheckCircleOutlined, + GithubOutlined, + MenuFoldOutlined, + SendOutlined, + WechatOutlined, + WarningOutlined +} from '@ant-design/icons'; import '../styles/sider-menu.css'; - +import { getLatestVersion } from '../api/config.ts'; +import ReactMarkdown from 'react-markdown'; const { Sider } = Layout; export interface SideNavProps { @@ -19,6 +27,28 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle const status = useSystemStatus(); const { token } = theme.useToken(); const [isModalOpen, setIsModalOpen] = useState(false); + const [isVersionModalOpen, setIsVersionModalOpen] = useState(false); + const [latestVersion, setLatestVersion] = useState<{ + version: string; + body: string; + } | null>(null); + + useEffect(() => { + getLatestVersion().then(resp => { + if (resp.latest_version && resp.body) { + setLatestVersion({ + version: resp.latest_version, + body: resp.body + }); + } + }); + }, []); + + const showVersionModal = () => { + setIsVersionModalOpen(true); + }; + + const hasUpdate = latestVersion && latestVersion.version !== status?.version; return ( <> -