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:
2026-04-17 07:20:10 +02:00
parent 8f72745bc0
commit 2bb80a8475
45 changed files with 5613 additions and 628 deletions
+18 -2
View File
@@ -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 };
});
},
},
+57
View File
@@ -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;
}