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