Add target comparison modal, integration goal progress, and session planning + full catalog expansion
Features added this session: - Target comparison: side-by-side overlay (CompareModal) from Targets page via ⊕ button on each row; shows altitude curves, key times, filter recommendations and per-filter integration progress for two targets simultaneously - Integration goal progress dashboard card: per-target keeper minutes vs goal hours (from CLAUDE.md §16.3) broken down by filter, with color-coded progress bars; powered by new stats.integration_goals backend query - Session planning timeline: Gantt-style "Plan Tonight" section on Dashboard (PlanningTimeline component) — search targets, set durations, sequential scheduling from dusk, overrun warnings, clipboard export - Slew-optimized run order toggle (nearest-neighbor sort by RA/Dec angular distance) - Best Nights 14-day card + Monthly Highlights card on Dashboard Catalog expansions: - Sharpless (Sh2), VdB, LDN, Barnard dark nebulae, LBN, Melotte, Collinder, Gum, RCW, Abell PN, Abell GC, PGC bright subset - Caldwell/Arp/Melotte/Collinder number columns + cross-reference maps - Weather score multiplier applied to composite sort - galaxy_cluster type (ACO badge) throughout TypeBadge, CSS, filter chips Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
HorizonPoint,
|
||||
LogEntry,
|
||||
Phd2Log,
|
||||
SimilarTarget,
|
||||
Stats,
|
||||
Target,
|
||||
TargetNotes,
|
||||
@@ -66,6 +67,7 @@ export interface TargetsParams {
|
||||
min_usable_min?: number;
|
||||
mosaic_only?: boolean;
|
||||
not_imaged?: boolean;
|
||||
show_custom?: boolean;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
@@ -84,6 +86,7 @@ export const api = {
|
||||
if (params.min_usable_min !== undefined) q.set('min_usable_min', String(params.min_usable_min));
|
||||
if (params.mosaic_only) q.set('mosaic_only', 'true');
|
||||
if (params.not_imaged) q.set('not_imaged', 'true');
|
||||
if (params.show_custom === false) q.set('show_custom', 'false');
|
||||
return get<TargetsResponse>(`/targets?${q}`);
|
||||
},
|
||||
get: (id: string): Promise<Target> => get(`/targets/${id}`),
|
||||
@@ -94,6 +97,7 @@ export const api = {
|
||||
yearly: (id: string): Promise<{ catalog_id: string; points: { date: string; alt_at_midnight: number; transit_alt: number; usable_min: number; moon_illumination: number }[] }> => get(`/targets/${id}/yearly`),
|
||||
getNotes: (id: string): Promise<TargetNotes> => get(`/targets/${id}/notes`),
|
||||
putNotes: (id: string, notes: string): Promise<{ status: string }> => put(`/targets/${id}/notes`, { notes }),
|
||||
similar: (id: string): Promise<{ similar: SimilarTarget[]; target_transit?: string }> => get(`/targets/${id}/similar`),
|
||||
},
|
||||
|
||||
tonight: {
|
||||
@@ -107,6 +111,10 @@ export const api = {
|
||||
get(`/calendar/${date}`),
|
||||
getNewMoonWindows: (): Promise<{ windows: { date: string; illumination: number; top_targets: { id: string; name: string; common_name?: string; max_alt_deg?: number; recommended_filter?: string }[] }[] }> =>
|
||||
get('/calendar/new-moon-windows'),
|
||||
getBestNights: (): Promise<{ nights: { date: string; score: number; moon_illumination: number; visible_count: number; avg_usable_min: number; top_targets: { id: string; name: string; common_name?: string; obj_type: string; max_alt_deg?: number; usable_min?: number; recommended_filter?: string }[] }[] }> =>
|
||||
get('/calendar/best-nights'),
|
||||
getMonthlyHighlights: (): Promise<{ month: string; highlights: { id: string; name: string; common_name?: string; obj_type: string; constellation?: string; peak_alt?: number; best_usable_min?: number; recommended_filter?: string; keeper_count: number }[] }> =>
|
||||
get('/calendar/monthly-highlights'),
|
||||
},
|
||||
|
||||
weather: {
|
||||
@@ -134,7 +142,11 @@ export const api = {
|
||||
delete: (id: number): Promise<{ status: string; id: number }> => del(`/phd2/${id}`),
|
||||
upload: (formData: FormData): Promise<{ id: number; duplicate: boolean; duplicate_id?: number; analysis: unknown; message?: string }> => {
|
||||
return fetch(`${base}/phd2/upload`, { method: 'POST', body: formData })
|
||||
.then(r => r.json() as Promise<{ id: number; duplicate: boolean; duplicate_id?: number; analysis: unknown; message?: string }>);
|
||||
.then(async r => {
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error((data as { error?: string }).error ?? `HTTP ${r.status}`);
|
||||
return data as { id: number; duplicate: boolean; duplicate_id?: number; analysis: unknown; message?: string };
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -146,7 +158,11 @@ export const api = {
|
||||
delete: (id: number): Promise<{ id: number }> => del(`/gallery/item/${id}`),
|
||||
upload: (catalogId: string, formData: FormData): Promise<{ id: number; url: string }> => {
|
||||
return fetch(`${base}/gallery/${catalogId}`, { method: 'POST', body: formData })
|
||||
.then(r => r.json() as Promise<{ id: number; url: string }>);
|
||||
.then(async r => {
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error((data as { error?: string }).error ?? `HTTP ${r.status}`);
|
||||
return data as { id: number; url: string };
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface Target {
|
||||
surface_brightness?: number;
|
||||
hubble_type?: string;
|
||||
messier_num?: number;
|
||||
caldwell_num?: number;
|
||||
arp_num?: number;
|
||||
is_highlight: boolean;
|
||||
fov_fill_pct?: number;
|
||||
mosaic_flag: boolean;
|
||||
@@ -32,6 +34,8 @@ export interface Target {
|
||||
moon_sep_deg?: number;
|
||||
is_visible_tonight?: boolean;
|
||||
total_integration_min?: number;
|
||||
is_custom?: boolean;
|
||||
urgency?: 'peak' | 'rising' | 'declining' | null;
|
||||
}
|
||||
|
||||
export interface TargetsResponse {
|
||||
@@ -164,6 +168,31 @@ export interface HorizonPoint {
|
||||
alt_deg: number;
|
||||
}
|
||||
|
||||
export interface IntegrationGap {
|
||||
catalog_id: string;
|
||||
name: string;
|
||||
common_name?: string;
|
||||
obj_type: string;
|
||||
sv220_min: number;
|
||||
c2_min: number;
|
||||
uvir_min: number;
|
||||
sv260_min: number;
|
||||
missing_filters: string[];
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
date: string;
|
||||
catalog_id: string;
|
||||
name: string;
|
||||
common_name?: string;
|
||||
obj_type?: string;
|
||||
filter_id: string;
|
||||
integration_min: number;
|
||||
quality: string;
|
||||
notes?: string;
|
||||
gallery_url?: string;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
total_sessions: number;
|
||||
total_integration_min: number;
|
||||
@@ -174,6 +203,10 @@ export interface Stats {
|
||||
quality: { quality: string; count: number }[];
|
||||
top_targets: { id: string; name: string; common_name?: string; obj_type: string; sessions: number; total_min: number }[];
|
||||
guiding: { date: string; rms_total?: number; rms_ra?: number; rms_dec?: number }[];
|
||||
integration_gaps: IntegrationGap[];
|
||||
history: HistoryEntry[];
|
||||
catalogue_completion: { name: string; total: number; keepers: number; pct: number }[];
|
||||
integration_goals: IntegrationGoal[];
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
@@ -230,8 +263,32 @@ export interface TargetNotes {
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SimilarTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
common_name?: string;
|
||||
obj_type: string;
|
||||
size_arcmin_maj?: number;
|
||||
fov_fill_pct?: number;
|
||||
messier_num?: number;
|
||||
max_alt_deg?: number;
|
||||
transit_utc?: string;
|
||||
recommended_filter?: string;
|
||||
}
|
||||
|
||||
export interface FilterBreakdownItem {
|
||||
filter_id: string;
|
||||
total_min: number;
|
||||
sessions: number;
|
||||
}
|
||||
|
||||
export interface IntegrationGoal {
|
||||
id: string;
|
||||
name: string;
|
||||
common_name?: string;
|
||||
obj_type: string;
|
||||
sv220_min: number;
|
||||
c2_min: number;
|
||||
uvir_min: number;
|
||||
sv260_min: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user