Initial Commit

This commit is contained in:
2026-04-09 23:23:31 +02:00
commit 9223e4d35f
94 changed files with 15173 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
import type {
CalendarDay,
CalendarDateDetail,
CurvePoint,
FilterBreakdownItem,
FilterRecommendation,
GalleryImage,
HorizonPoint,
LogEntry,
Phd2Log,
Stats,
Target,
TargetNotes,
TargetsResponse,
Tonight,
VisibilitySummary,
WeatherData,
Workflow,
} from './types';
const base = '/api';
async function get<T>(path: string): Promise<T> {
const resp = await fetch(`${base}${path}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${path}`);
return resp.json() as Promise<T>;
}
async function post<T>(path: string, body: unknown): Promise<T> {
const resp = await fetch(`${base}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${path}`);
return resp.json() as Promise<T>;
}
async function put<T>(path: string, body: unknown): Promise<T> {
const resp = await fetch(`${base}${path}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${path}`);
return resp.json() as Promise<T>;
}
async function del<T>(path: string): Promise<T> {
const resp = await fetch(`${base}${path}`, { method: 'DELETE' });
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${path}`);
return resp.json() as Promise<T>;
}
// Targets
export interface TargetsParams {
type?: string;
constellation?: string;
filter?: string;
tonight?: boolean;
search?: string;
sort?: string;
page?: number;
limit?: number;
min_alt_deg?: number;
min_usable_min?: number;
mosaic_only?: boolean;
not_imaged?: boolean;
}
export const api = {
targets: {
list: (params: TargetsParams = {}): Promise<TargetsResponse> => {
const q = new URLSearchParams();
if (params.type) q.set('type', params.type);
if (params.constellation) q.set('constellation', params.constellation);
if (params.filter) q.set('filter', params.filter);
if (params.tonight !== undefined) q.set('tonight', String(params.tonight));
if (params.search) q.set('search', params.search);
if (params.sort) q.set('sort', params.sort);
if (params.page) q.set('page', String(params.page));
if (params.limit) q.set('limit', String(params.limit));
if (params.min_alt_deg !== undefined) q.set('min_alt_deg', String(params.min_alt_deg));
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');
return get<TargetsResponse>(`/targets?${q}`);
},
get: (id: string): Promise<Target> => get(`/targets/${id}`),
visibility: (id: string): Promise<VisibilitySummary> => get(`/targets/${id}/visibility`),
curve: (id: string): Promise<{ catalog_id: string; curve: CurvePoint[] }> => get(`/targets/${id}/curve`),
filters: (id: string): Promise<{ recommendations: FilterRecommendation[] }> => get(`/targets/${id}/filters`),
workflow: (id: string, filterId: string): Promise<Workflow> => get(`/targets/${id}/workflow/${filterId}`),
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 }),
},
tonight: {
get: (): Promise<Tonight> => get('/tonight'),
},
calendar: {
get: (months?: number): Promise<{ days: CalendarDay[] }> =>
get(`/calendar${months ? `?months=${months}` : ''}`),
getDate: (date: string): Promise<CalendarDateDetail> =>
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'),
},
weather: {
get: (): Promise<WeatherData> => get('/weather'),
forecast: (): Promise<unknown> => get('/weather/forecast'),
},
log: {
list: (page?: number): Promise<{ items: LogEntry[]; total: number }> =>
get(`/log${page ? `?page=${page}` : ''}`),
forTarget: (catalogId: string): Promise<{ items: LogEntry[]; total_integration_min: number; filter_breakdown: FilterBreakdownItem[] }> =>
get(`/log/${catalogId}`),
create: (entry: Omit<LogEntry, 'id' | 'created_at' | 'target_name' | 'target_common_name' | 'target_obj_type'>): Promise<{ id: number }> =>
post('/log', entry),
update: (id: number, data: Partial<LogEntry>): Promise<{ id: number }> =>
put(`/log/entry/${id}`, data),
delete: (id: number): Promise<{ id: number }> =>
del(`/log/entry/${id}`),
exportCsv: (): void => { window.open('/api/log/export', '_blank'); },
},
phd2: {
list: (): Promise<{ items: Phd2Log[] }> => get('/phd2'),
get: (id: number): Promise<Phd2Log> => get(`/phd2/${id}`),
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 }>);
},
},
gallery: {
listAll: (): Promise<{ items: (GalleryImage & { target_name?: string; target_common_name?: string })[] }> =>
get('/gallery'),
list: (catalogId: string): Promise<{ items: GalleryImage[] }> =>
get(`/gallery/${catalogId}`),
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 }>);
},
},
horizon: {
get: (): Promise<{ points: HorizonPoint[] }> => get('/horizon'),
set: (points: HorizonPoint[]): Promise<{ status: string }> => put('/horizon', points),
},
stats: {
get: (): Promise<Stats> => get('/stats'),
},
health: {
get: (): Promise<{ status: string; catalog_size: number; catalog_last_refreshed?: number; db_size_bytes?: number; version: string }> => get('/health'),
},
admin: {
catalogRefresh: (): Promise<{ status: string }> =>
fetch('/api/catalog/refresh', { method: 'POST' }).then(r => r.json() as Promise<{ status: string }>),
nightlyRecompute: (): Promise<{ status: string }> =>
fetch('/api/nightly/recompute', { method: 'POST' }).then(r => r.json() as Promise<{ status: string }>),
},
};
+237
View File
@@ -0,0 +1,237 @@
export interface Target {
id: string;
name: string;
common_name?: string;
obj_type: string;
ra_deg: number;
dec_deg: number;
ra_h: string;
dec_dms: string;
constellation?: string;
size_arcmin_maj?: number;
size_arcmin_min?: number;
pos_angle_deg?: number;
mag_v?: number;
surface_brightness?: number;
hubble_type?: string;
messier_num?: number;
is_highlight: boolean;
fov_fill_pct?: number;
mosaic_flag: boolean;
mosaic_panels_w: number;
mosaic_panels_h: number;
difficulty?: number;
guide_star_density?: string;
// From nightly_cache
max_alt_deg?: number;
usable_min?: number;
transit_utc?: string;
recommended_filter?: string;
best_start_utc?: string;
best_end_utc?: string;
moon_sep_deg?: number;
is_visible_tonight?: boolean;
total_integration_min?: number;
}
export interface TargetsResponse {
items: Target[];
total: number;
page: number;
limit: number;
}
export interface VisibilitySummary {
catalog_id: string;
night_date?: string;
max_alt_deg?: number;
transit_utc?: string;
rise_utc?: string;
set_utc?: string;
best_start_utc?: string;
best_end_utc?: string;
usable_min?: number;
meridian_flip_utc?: string;
airmass_at_transit?: number;
extinction_mag?: number;
moon_sep_deg?: number;
recommended_filter?: string;
is_visible_tonight?: boolean;
}
export interface CurvePoint {
utc: string;
alt_deg: number;
az_deg: number;
airmass: number;
above_custom_horizon: boolean;
moon_alt_deg: number;
}
export interface Tonight {
date: string;
astro_dusk_utc: string;
astro_dawn_utc: string;
moon_rise_utc?: string;
moon_set_utc?: string;
moon_illumination?: number;
moon_phase_name?: string;
moon_ra_deg?: number;
moon_dec_deg?: number;
true_dark_start_utc?: string;
true_dark_end_utc?: string;
true_dark_minutes?: number;
computed_at?: number;
}
export interface WeatherData {
dew_point_c?: number;
temp_c?: number;
humidity_pct?: number;
go_nogo?: 'go' | 'marginal' | 'nogo';
go_nogo_reasons?: string[];
fetched_at?: number;
dew_alert?: 'warning' | 'critical';
cloudcover?: number;
seeing?: number;
transparency?: number;
lifted_index?: number;
wind10m?: { speed: number; direction: string };
rh2m?: number;
}
export interface FilterRecommendation {
filter_id: string;
filter_name: string;
suitability: 'ideal' | 'good' | 'marginal' | 'unsuitable';
reason: string;
warning?: string;
est_integration_hours?: number;
sessions_needed?: number;
exposure_sec?: number;
frames_needed?: number;
}
export interface LogEntry {
id: number;
catalog_id: string;
session_date: string;
filter_id: string;
integration_min: number;
quality: 'keeper' | 'needs_more' | 'rejected' | 'pending';
notes?: string;
guiding_rms?: number;
mean_temp_c?: number;
created_at: number;
target_name?: string;
target_common_name?: string;
target_obj_type?: string;
}
export interface Phd2Log {
id: number;
session_date: string;
filename: string;
rms_total?: number;
rms_ra?: number;
rms_dec?: number;
peak_error?: number;
star_lost_count?: number;
duration_min?: number;
guide_star_snr?: number;
created_at: number;
// Equipment details
equipment_profile?: string;
camera_name?: string;
exposure_ms?: number;
mount_name?: string;
pixel_scale_arcsec?: number;
hfd_px?: number;
guide_star_snr_at_start?: number;
}
export interface GalleryImage {
id: number;
catalog_id: string;
filename: string;
url: string;
caption?: string;
created_at: number;
}
export interface HorizonPoint {
az_deg: number;
alt_deg: number;
}
export interface Stats {
total_sessions: number;
total_integration_min: number;
objects_with_keeper: number;
filter_usage: { filter_id: string; count: number; total_min: number }[];
monthly: { month: string; sessions: number; total_min: number }[];
by_type: { obj_type: string; sessions: number; total_min: number }[];
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 }[];
}
export interface Workflow {
name: string;
steps: string[];
plugins: [string, string][];
notes: string;
}
export interface CalendarDay {
date: string;
visible_count?: number;
max_usable_min?: number;
avg_max_alt?: number;
moon_illumination?: number;
}
export interface CalendarDateDetail {
date: string;
moon_illumination: number;
top_targets: {
id: string;
name: string;
common_name?: string;
obj_type: string;
max_alt_deg?: number;
usable_min?: number;
transit_utc?: string;
recommended_filter?: string;
}[];
tonight?: {
astro_dusk_utc?: string;
astro_dawn_utc?: string;
moon_rise_utc?: string;
moon_set_utc?: string;
moon_illumination?: number;
moon_phase_name?: string;
true_dark_start_utc?: string;
true_dark_end_utc?: string;
true_dark_minutes?: number;
};
weather?: {
go_nogo?: string;
temp_c?: number;
dew_point_c?: number;
cloudcover?: number;
seeing?: number;
transparency?: number;
};
}
export interface TargetNotes {
catalog_id: string;
notes: string;
}
export interface FilterBreakdownItem {
filter_id: string;
total_min: number;
sessions: number;
}