增加了数据截止日
parent
1c607edc13
commit
9b0614e7a1
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue