153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { api } from '../../api';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
interface UploadResult {
|
|
filename: string;
|
|
rms_total: number;
|
|
rms_ra: number;
|
|
rms_dec: number;
|
|
duration_min?: number;
|
|
camera_name?: string;
|
|
exposure_ms?: number;
|
|
mount_name?: string;
|
|
session_date?: string;
|
|
error?: string;
|
|
duplicate?: { id: number; message: string };
|
|
}
|
|
|
|
interface Props {
|
|
onUploaded?: (id: number) => void;
|
|
}
|
|
|
|
export default function PHD2UploadZone({ onUploaded }: Props) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [results, setResults] = useState<UploadResult[]>([]);
|
|
const qc = useQueryClient();
|
|
|
|
const handleFiles = async (files: FileList | null) => {
|
|
if (!files || files.length === 0) return;
|
|
|
|
setUploading(true);
|
|
setResults([]);
|
|
|
|
const uploadedIds: number[] = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
|
|
try {
|
|
const res = await api.phd2.upload(fd);
|
|
|
|
if (res.duplicate) {
|
|
setResults(prev => [...prev, {
|
|
filename: file.name,
|
|
rms_total: 0,
|
|
rms_ra: 0,
|
|
rms_dec: 0,
|
|
duplicate: {
|
|
id: res.duplicate_id || 0,
|
|
message: res.message || `Duplicate session detected (ID: ${res.duplicate_id})`
|
|
}
|
|
}]);
|
|
} else {
|
|
const analysis = res.analysis as any;
|
|
setResults(prev => [...prev, {
|
|
filename: file.name,
|
|
rms_total: analysis.rms_total_arcsec,
|
|
rms_ra: analysis.rms_ra_arcsec,
|
|
rms_dec: analysis.rms_dec_arcsec,
|
|
duration_min: analysis.duration_min,
|
|
camera_name: analysis.camera_name,
|
|
exposure_ms: analysis.exposure_ms,
|
|
mount_name: analysis.mount_name,
|
|
session_date: analysis.session_date,
|
|
}]);
|
|
uploadedIds.push(res.id);
|
|
}
|
|
} catch (e) {
|
|
setResults(prev => [...prev, {
|
|
filename: file.name,
|
|
rms_total: 0,
|
|
rms_ra: 0,
|
|
rms_dec: 0,
|
|
error: `Parse failed: ${e instanceof Error ? e.message : 'Unknown error'}`
|
|
}]);
|
|
}
|
|
}
|
|
|
|
qc.invalidateQueries({ queryKey: ['phd2'] });
|
|
uploadedIds.forEach(id => onUploaded?.(id));
|
|
setUploading(false);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
onClick={() => inputRef.current?.click()}
|
|
style={{
|
|
border: '1px dashed var(--border)',
|
|
borderRadius: 3,
|
|
padding: '10px 14px',
|
|
cursor: 'pointer',
|
|
color: 'var(--text-lo)',
|
|
fontFamily: 'var(--font-mono)',
|
|
fontSize: 11,
|
|
background: 'var(--bg-deep)',
|
|
}}
|
|
>
|
|
{uploading ? 'Parsing PHD2 logs...' : '↑ Upload PHD2 log(s) (.log)'}
|
|
</div>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept=".log,.csv"
|
|
multiple
|
|
style={{ display: 'none' }}
|
|
onChange={e => handleFiles(e.target.files)}
|
|
/>
|
|
{results.map((result, idx) => (
|
|
<div key={idx} style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border)' }}>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', marginBottom: 4 }}>
|
|
{result.filename}
|
|
</div>
|
|
|
|
{result.error && (
|
|
<div style={{ color: 'var(--danger)', fontSize: 11 }}>
|
|
✗ {result.error}
|
|
</div>
|
|
)}
|
|
|
|
{result.duplicate && (
|
|
<div style={{ color: 'var(--warn)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>
|
|
⚠ {result.duplicate.message}
|
|
</div>
|
|
)}
|
|
|
|
{!result.error && !result.duplicate && (
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--good)', lineHeight: '1.5' }}>
|
|
<div>✓ RMS: {result.rms_total.toFixed(2)}″ (RA: {result.rms_ra.toFixed(2)}″ Dec: {result.rms_dec.toFixed(2)}″)</div>
|
|
{result.session_date && (
|
|
<div style={{ color: 'var(--text-mid)', marginTop: 2 }}>Date: {result.session_date}</div>
|
|
)}
|
|
{result.duration_min !== undefined && (
|
|
<div style={{ color: 'var(--text-mid)', marginTop: 2 }}>Duration: {result.duration_min}m</div>
|
|
)}
|
|
{(result.camera_name || result.mount_name) && (
|
|
<div style={{ color: 'var(--text-lo)', marginTop: 2, fontSize: 10 }}>
|
|
{result.camera_name && <div>Camera: {result.camera_name}</div>}
|
|
{result.mount_name && <div>Mount: {result.mount_name}</div>}
|
|
{result.exposure_ms && <div>Exposure: {result.exposure_ms}ms</div>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|