增加了数据截止日

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:*)",
"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": []

View File

@ -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)}"
)

View File

@ -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<any>(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() {
<Header
bodyCount={bodies.filter(b => 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 && (
<TimelineController
onTimeChange={handleTimeChange}
maxDate={new Date()} // Start point (now)
minDate={new Date(Date.now() - TIMELINE_DAYS * 24 * 60 * 60 * 1000)} // End point (past)
maxDate={cutoffDate} // Use cutoff date instead of new Date()
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 {
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 (
<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">
@ -25,7 +32,14 @@ export function Header({
<div className="flex items-center gap-3">
<span className="text-4xl">🌌</span>
<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>
</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 { fetchCelestialPositions } from '../utils/api';
import type { CelestialBody } from '../types';
import { useDataCutoffDate } from './useDataCutoffDate';
export function useSpaceData() {
const [bodies, setBodies] = useState<CelestialBody[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 };
}