增加了数据截止日

main
mula.liu 2025-12-03 14:26:53 +08:00
parent 1c607edc13
commit 9b0614e7a1
6 changed files with 148 additions and 17 deletions

View File

@ -42,7 +42,8 @@
"Bash(timeout 5 curl:*)", "Bash(timeout 5 curl:*)",
"Read(//tmp/**)", "Read(//tmp/**)",
"Read(//Users/jiliu/WorkSpace/**)", "Read(//Users/jiliu/WorkSpace/**)",
"Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend psql:*)" "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend psql:*)",
"Bash(git add:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -3,7 +3,9 @@ System Settings API Routes
""" """
from fastapi import APIRouter, HTTPException, Query, Depends, status from fastapi import APIRouter, HTTPException, Query, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from datetime import datetime
import logging import logging
from pydantic import BaseModel 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.redis_cache import redis_cache
from app.services.cache import cache_service from app.services.cache import cache_service
from app.database import get_db from app.database import get_db
from app.models.db import Position
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -251,3 +254,51 @@ async def initialize_default_settings(
await db.commit() await db.commit()
return {"message": "Default settings initialized successfully"} 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)}"
)

View File

@ -4,6 +4,7 @@ import { useSpaceData } from './hooks/useSpaceData';
import { useHistoricalData } from './hooks/useHistoricalData'; import { useHistoricalData } from './hooks/useHistoricalData';
import { useTrajectory } from './hooks/useTrajectory'; import { useTrajectory } from './hooks/useTrajectory';
import { useScreenshot } from './hooks/useScreenshot'; import { useScreenshot } from './hooks/useScreenshot';
import { useDataCutoffDate } from './hooks/useDataCutoffDate';
import { Header } from './components/Header'; import { Header } from './components/Header';
import { Scene } from './components/Scene'; import { Scene } from './components/Scene';
import { ProbeList } from './components/ProbeList'; import { ProbeList } from './components/ProbeList';
@ -63,6 +64,9 @@ function App() {
const [user, setUser] = useState<any>(auth.getUser()); const [user, setUser] = useState<any>(auth.getUser());
const [showAuthModal, setShowAuthModal] = useState(false); const [showAuthModal, setShowAuthModal] = useState(false);
// Get data cutoff date
const { cutoffDate } = useDataCutoffDate();
// Use real-time data or historical data based on mode // Use real-time data or historical data based on mode
const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData(); const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData();
const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate); const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate);
@ -83,12 +87,12 @@ function App() {
const toggleTimelineMode = useCallback(() => { const toggleTimelineMode = useCallback(() => {
setIsTimelineMode((prev) => !prev); setIsTimelineMode((prev) => !prev);
if (!isTimelineMode) { if (!isTimelineMode) {
// Entering timeline mode, set initial date to now (will play backward) // Entering timeline mode, use cutoff date instead of current date
setSelectedDate(new Date()); setSelectedDate(cutoffDate || new Date());
} else { } else {
setSelectedDate(null); setSelectedDate(null);
} }
}, [isTimelineMode]); }, [isTimelineMode, cutoffDate]);
// Filter probes and planets from all bodies // Filter probes and planets from all bodies
const probes = bodies.filter((b) => b.type === 'probe'); const probes = bodies.filter((b) => b.type === 'probe');
@ -150,6 +154,7 @@ function App() {
<Header <Header
bodyCount={bodies.filter(b => b.is_active !== false).length} bodyCount={bodies.filter(b => b.is_active !== false).length}
selectedBodyName={selectedBody?.name} selectedBodyName={selectedBody?.name}
cutoffDate={cutoffDate}
user={user} user={user}
onOpenAuth={() => setShowAuthModal(true)} onOpenAuth={() => setShowAuthModal(true)}
onLogout={handleLogout} onLogout={handleLogout}
@ -211,11 +216,11 @@ function App() {
/> />
{/* Timeline Controller */} {/* Timeline Controller */}
{isTimelineMode && ( {isTimelineMode && cutoffDate && (
<TimelineController <TimelineController
onTimeChange={handleTimeChange} onTimeChange={handleTimeChange}
maxDate={new Date()} // Start point (now) maxDate={cutoffDate} // Use cutoff date instead of new Date()
minDate={new Date(Date.now() - TIMELINE_DAYS * 24 * 60 * 60 * 1000)} // End point (past) minDate={new Date(cutoffDate.getTime() - TIMELINE_DAYS * 24 * 60 * 60 * 1000)} // End point (past)
/> />
)} )}

View File

@ -3,20 +3,27 @@ import { UserAuth } from './UserAuth';
interface HeaderProps { interface HeaderProps {
selectedBodyName?: string; selectedBodyName?: string;
bodyCount: number; bodyCount: number;
cutoffDate?: Date | null;
user?: any; user?: any;
onOpenAuth?: () => void; onOpenAuth?: () => void;
onLogout?: () => void; onLogout?: () => void;
onNavigateToAdmin?: () => void; onNavigateToAdmin?: () => void;
} }
export function Header({ export function Header({
selectedBodyName, selectedBodyName,
bodyCount, bodyCount,
cutoffDate,
user, user,
onOpenAuth, onOpenAuth,
onLogout, onLogout,
onNavigateToAdmin onNavigateToAdmin
}: HeaderProps) { }: 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 ( return (
<header className="absolute top-0 left-0 right-0 z-50 pointer-events-none"> <header className="absolute top-0 left-0 right-0 z-50 pointer-events-none">
<div className="px-6 py-4 bg-gradient-to-b from-black/90 via-black/60 to-transparent flex items-start justify-between"> <div className="px-6 py-4 bg-gradient-to-b from-black/90 via-black/60 to-transparent flex items-start justify-between">
@ -25,7 +32,14 @@ export function Header({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">🌌</span> <span className="text-4xl">🌌</span>
<div> <div>
<h1 className="text-2xl font-bold text-white tracking-tight drop-shadow-md">Cosmo</h1> <div className="flex items-baseline gap-2">
<h1 className="text-2xl font-bold text-white tracking-tight drop-shadow-md">Cosmo</h1>
{formattedCutoffDate && (
<span className="text-xs text-gray-400 font-mono">
( {formattedCutoffDate})
</span>
)}
</div>
<p className="text-xs text-gray-400 font-medium tracking-wide">DEEP SPACE EXPLORER</p> <p className="text-xs text-gray-400 font-medium tracking-wide">DEEP SPACE EXPLORER</p>
</div> </div>
</div> </div>

View File

@ -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<Date | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchCutoffDate = async () => {
try {
setLoading(true);
const response = await api.get<CutoffDateResponse>('/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 };
}

View File

@ -4,26 +4,35 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { fetchCelestialPositions } from '../utils/api'; import { fetchCelestialPositions } from '../utils/api';
import type { CelestialBody } from '../types'; import type { CelestialBody } from '../types';
import { useDataCutoffDate } from './useDataCutoffDate';
export function useSpaceData() { export function useSpaceData() {
const [bodies, setBodies] = useState<CelestialBody[]>([]); const [bodies, setBodies] = useState<CelestialBody[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Get data cutoff date from backend
const { cutoffDate, loading: cutoffLoading } = useDataCutoffDate();
useEffect(() => { useEffect(() => {
// Wait for cutoff date to be loaded
if (cutoffLoading || !cutoffDate) {
return;
}
async function loadData() { async function loadData() {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
// Fetch current position - single point in time for today (UTC midnight) // Use cutoff date instead of current date
// This ensures we hit the cache if we already have data for today // Set to UTC midnight for consistency
const now = new Date(); const targetDate = new Date(cutoffDate);
now.setUTCHours(0, 0, 0, 0); targetDate.setUTCHours(0, 0, 0, 0);
const data = await fetchCelestialPositions( const data = await fetchCelestialPositions(
now.toISOString(), targetDate.toISOString(),
now.toISOString(), // Same as start - single point in time targetDate.toISOString(), // Same as start - single point in time
'1d' // Use 1d step for consistency '1d' // Use 1d step for consistency
); );
@ -37,7 +46,7 @@ export function useSpaceData() {
} }
loadData(); loadData();
}, []); }, [cutoffDate, cutoffLoading]);
return { bodies, loading, error }; return { bodies, loading, error };
} }