From 9b0614e7a1ef18bbf209fadf1b242cc1cb84bb21 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Wed, 3 Dec 2025 14:26:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=88=AA=E6=AD=A2=E6=97=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- backend/app/api/system.py | 51 +++++++++++++++++++++++++ frontend/src/App.tsx | 17 ++++++--- frontend/src/components/Header.tsx | 20 ++++++++-- frontend/src/hooks/useDataCutoffDate.ts | 51 +++++++++++++++++++++++++ frontend/src/hooks/useSpaceData.ts | 23 +++++++---- 6 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 frontend/src/hooks/useDataCutoffDate.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 678ed6b..ef86546 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -42,7 +42,8 @@ "Bash(timeout 5 curl:*)", "Read(//tmp/**)", "Read(//Users/jiliu/WorkSpace/**)", - "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend psql:*)" + "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend psql:*)", + "Bash(git add:*)" ], "deny": [], "ask": [] diff --git a/backend/app/api/system.py b/backend/app/api/system.py index d05afeb..4156cd5 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -3,7 +3,9 @@ System Settings API Routes """ from fastapi import APIRouter, HTTPException, Query, Depends, status from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func from typing import Optional, Dict, Any, List +from datetime import datetime import logging from pydantic import BaseModel @@ -11,6 +13,7 @@ from app.services.system_settings_service import system_settings_service from app.services.redis_cache import redis_cache from app.services.cache import cache_service from app.database import get_db +from app.models.db import Position logger = logging.getLogger(__name__) @@ -251,3 +254,51 @@ async def initialize_default_settings( await db.commit() return {"message": "Default settings initialized successfully"} + + +@router.get("/data-cutoff-date") +async def get_data_cutoff_date( + db: AsyncSession = Depends(get_db) +): + """ + Get the data cutoff date based on the Sun's (ID=10) last available data + + This endpoint returns the latest date for which we have position data + in the database. It's used by the frontend to determine: + - The current date to display on the homepage + - The maximum date for timeline playback + + Returns: + - cutoff_date: ISO format date string (YYYY-MM-DD) + - timestamp: Unix timestamp + - datetime: Full ISO datetime string + """ + try: + # Query the latest position data for the Sun (body_id = 10) + stmt = select(func.max(Position.time)).where( + Position.body_id == '10' + ) + result = await db.execute(stmt) + latest_time = result.scalar_one_or_none() + + if latest_time is None: + # No data available, return current date as fallback + logger.warning("No position data found for Sun (ID=10), using current date as fallback") + latest_time = datetime.utcnow() + + # Format the response + cutoff_date = latest_time.strftime("%Y-%m-%d") + + return { + "cutoff_date": cutoff_date, + "timestamp": int(latest_time.timestamp()), + "datetime": latest_time.isoformat(), + "message": "Data cutoff date retrieved successfully" + } + + except Exception as e: + logger.error(f"Error retrieving data cutoff date: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve data cutoff date: {str(e)}" + ) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cb601b4..fa38102 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { useSpaceData } from './hooks/useSpaceData'; import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; import { useScreenshot } from './hooks/useScreenshot'; +import { useDataCutoffDate } from './hooks/useDataCutoffDate'; import { Header } from './components/Header'; import { Scene } from './components/Scene'; import { ProbeList } from './components/ProbeList'; @@ -63,6 +64,9 @@ function App() { const [user, setUser] = useState(auth.getUser()); const [showAuthModal, setShowAuthModal] = useState(false); + // Get data cutoff date + const { cutoffDate } = useDataCutoffDate(); + // Use real-time data or historical data based on mode const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData(); const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate); @@ -83,12 +87,12 @@ function App() { const toggleTimelineMode = useCallback(() => { setIsTimelineMode((prev) => !prev); if (!isTimelineMode) { - // Entering timeline mode, set initial date to now (will play backward) - setSelectedDate(new Date()); + // Entering timeline mode, use cutoff date instead of current date + setSelectedDate(cutoffDate || new Date()); } else { setSelectedDate(null); } - }, [isTimelineMode]); + }, [isTimelineMode, cutoffDate]); // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); @@ -150,6 +154,7 @@ function App() {
b.is_active !== false).length} selectedBodyName={selectedBody?.name} + cutoffDate={cutoffDate} user={user} onOpenAuth={() => setShowAuthModal(true)} onLogout={handleLogout} @@ -211,11 +216,11 @@ function App() { /> {/* Timeline Controller */} - {isTimelineMode && ( + {isTimelineMode && cutoffDate && ( )} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 8028317..2fbac36 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -3,20 +3,27 @@ import { UserAuth } from './UserAuth'; interface HeaderProps { selectedBodyName?: string; bodyCount: number; + cutoffDate?: Date | null; user?: any; onOpenAuth?: () => void; onLogout?: () => void; onNavigateToAdmin?: () => void; } -export function Header({ - selectedBodyName, +export function Header({ + selectedBodyName, bodyCount, + cutoffDate, user, onOpenAuth, onLogout, onNavigateToAdmin }: HeaderProps) { + // Format cutoff date as YYYY/MM/DD + const formattedCutoffDate = cutoffDate + ? `${cutoffDate.getFullYear()}/${String(cutoffDate.getMonth() + 1).padStart(2, '0')}/${String(cutoffDate.getDate()).padStart(2, '0')}` + : ''; + return (
@@ -25,7 +32,14 @@ export function Header({
🌌
-

Cosmo

+
+

Cosmo

+ {formattedCutoffDate && ( + + (截止日期 {formattedCutoffDate}) + + )} +

DEEP SPACE EXPLORER

diff --git a/frontend/src/hooks/useDataCutoffDate.ts b/frontend/src/hooks/useDataCutoffDate.ts new file mode 100644 index 0000000..de76198 --- /dev/null +++ b/frontend/src/hooks/useDataCutoffDate.ts @@ -0,0 +1,51 @@ +/** + * Hook to get the data cutoff date from backend + * + * This hook fetches the latest available data date from the backend, + * which is determined by the Sun's (ID=10) last position data in the database. + * + * This date is used to: + * - Set the current date on the homepage instead of system time + * - Set the maximum date for timeline playback + */ +import { useState, useEffect } from 'react'; +import { api } from '../utils/api'; + +interface CutoffDateResponse { + cutoff_date: string; // YYYY-MM-DD format + timestamp: number; + datetime: string; // Full ISO datetime + message: string; +} + +export function useDataCutoffDate() { + const [cutoffDate, setCutoffDate] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchCutoffDate = async () => { + try { + setLoading(true); + const response = await api.get('/system/data-cutoff-date'); + const data = response.data; + + // Convert to Date object + const date = new Date(data.datetime); + setCutoffDate(date); + setError(null); + } catch (err) { + console.error('Failed to fetch data cutoff date:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + // Fallback to current date + setCutoffDate(new Date()); + } finally { + setLoading(false); + } + }; + + fetchCutoffDate(); + }, []); + + return { cutoffDate, loading, error }; +} diff --git a/frontend/src/hooks/useSpaceData.ts b/frontend/src/hooks/useSpaceData.ts index ed1fffe..8b9b899 100644 --- a/frontend/src/hooks/useSpaceData.ts +++ b/frontend/src/hooks/useSpaceData.ts @@ -4,26 +4,35 @@ import { useState, useEffect } from 'react'; import { fetchCelestialPositions } from '../utils/api'; import type { CelestialBody } from '../types'; +import { useDataCutoffDate } from './useDataCutoffDate'; export function useSpaceData() { const [bodies, setBodies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + // Get data cutoff date from backend + const { cutoffDate, loading: cutoffLoading } = useDataCutoffDate(); + useEffect(() => { + // Wait for cutoff date to be loaded + if (cutoffLoading || !cutoffDate) { + return; + } + async function loadData() { try { setLoading(true); setError(null); - // Fetch current position - single point in time for today (UTC midnight) - // This ensures we hit the cache if we already have data for today - const now = new Date(); - now.setUTCHours(0, 0, 0, 0); + // Use cutoff date instead of current date + // Set to UTC midnight for consistency + const targetDate = new Date(cutoffDate); + targetDate.setUTCHours(0, 0, 0, 0); const data = await fetchCelestialPositions( - now.toISOString(), - now.toISOString(), // Same as start - single point in time + targetDate.toISOString(), + targetDate.toISOString(), // Same as start - single point in time '1d' // Use 1d step for consistency ); @@ -37,7 +46,7 @@ export function useSpaceData() { } loadData(); - }, []); + }, [cutoffDate, cutoffLoading]); return { bodies, loading, error }; }