Added next rocket data
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
GeorgeWebberley 2026-01-28 20:39:18 +01:00
parent 0c5c3cf2d0
commit 1f43a7c29e
7 changed files with 199 additions and 54 deletions

View file

@ -12,6 +12,9 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/lib ./lib COPY --from=builder /app/lib ./lib
COPY --from=builder /app/config ./config
RUN mkdir -p /app/data
EXPOSE 3000 EXPOSE 3000
CMD ["node", "server.js"] CMD ["node", "server.js"]

View file

@ -2,7 +2,6 @@ import MissionControl from '@/components/MissionControl';
import db from '@/lib/db'; import db from '@/lib/db';
export default function Home() { export default function Home() {
// SQLite handles the 'now' comparison perfectly
const issRow = db.prepare(` const issRow = db.prepare(`
SELECT pass_time, end_time SELECT pass_time, end_time
FROM iss_passes FROM iss_passes
@ -10,26 +9,41 @@ export default function Home() {
ORDER BY datetime(pass_time) ASC LIMIT 1 ORDER BY datetime(pass_time) ASC LIMIT 1
`).get() as { pass_time: string, end_time: string } | undefined; `).get() as { pass_time: string, end_time: string } | undefined;
// 2. Fetch Moon
const moonRow = db.prepare(` const moonRow = db.prepare(`
SELECT title, event_time SELECT title, event_time
FROM global_events FROM global_events
WHERE id = 'moon_phase' WHERE id = 'moon_phase'
`).get() as { title: string, event_time: string } | undefined; `).get() as { title: string, event_time: string } | undefined;
// Prepare Dates const cosmicRow = db.prepare(`
SELECT title, event_time
FROM global_events
WHERE id = 'next_cosmic_event'
`).get() as { title: string, event_time: string } | undefined;
const launchRow = db.prepare(
"SELECT title, event_time FROM global_events WHERE id = 'next_launch'"
).get() as { title: string, event_time: string } | undefined;
const launchStart = launchRow ? new Date(launchRow.event_time) : null;
const launchEnd = launchStart ? new Date(launchStart.getTime() + 7200000) : null;
const issStart = issRow ? new Date(issRow.pass_time) : null; const issStart = issRow ? new Date(issRow.pass_time) : null;
const issEnd = issRow ? new Date(issRow.end_time) : null; const issEnd = issRow ? new Date(issRow.end_time) : null;
const moonStart = moonRow ? new Date(moonRow.event_time) : null; const moonStart = moonRow ? new Date(moonRow.event_time) : null;
// Moon "Event" lasts 24 hours
const moonEnd = moonStart ? new Date(moonStart.getTime() + 86400000) : null; const moonEnd = moonStart ? new Date(moonStart.getTime() + 86400000) : null;
const cosmicStart = cosmicRow ? new Date(cosmicRow.event_time) : null;
const cosmicEnd = cosmicStart ? new Date(cosmicStart.getTime() + 14400000) : null;
return ( return (
<main> <main>
<MissionControl <MissionControl
iss={{ start: issStart, end: issEnd }} iss={{ start: issStart, end: issEnd }}
moon={{ title: moonRow?.title || "Moon Phase", start: moonStart, end: moonEnd }} moon={{ title: moonRow?.title || "Moon Phase", start: moonStart, end: moonEnd }}
cosmic={{ title: cosmicRow?.title || "Cosmic Event", start: cosmicStart, end: cosmicEnd }}
launch={{ title: launchRow?.title || "Rocket Launch", start: launchStart, end: launchEnd }}
/> />
</main> </main>
); );

View file

@ -112,20 +112,17 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
<div className="text-4xl font-mono text-white flex items-baseline gap-2"> <div className="text-4xl font-mono text-white flex items-baseline gap-2">
{parseInt(timeLeft.d) >= 2 ? ( {parseInt(timeLeft.d) >= 2 ? (
/* Case 1: More than 2 days - Keep it super simple */
<> <>
<span>{timeLeft.d}</span> <span>{timeLeft.d}</span>
<span className="text-slate-500 text-xl lowercase">Days</span> <span className="text-slate-500 text-xl lowercase">Days</span>
</> </>
) : parseInt(timeLeft.d) === 1 ? ( ) : parseInt(timeLeft.d) === 1 ? (
/* Case 2: Between 24 and 48 hours - Show Day + Hours */
<> <>
<span>01</span><span className="text-slate-500 text-sm">d</span> <span>01</span><span className="text-slate-500 text-sm">d</span>
<span className="text-slate-500">:</span> <span className="text-slate-500">:</span>
<span>{timeLeft.h}</span><span className="text-slate-500 text-sm">h</span> <span>{timeLeft.h}</span><span className="text-slate-500 text-sm">h</span>
</> </>
) : ( ) : (
/* Case 3: Under 24 hours - The full high-precision clock */
<> <>
<span>{timeLeft.h}</span> <span>{timeLeft.h}</span>
<span className="text-slate-500">:</span> <span className="text-slate-500">:</span>
@ -135,9 +132,18 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
</> </>
)} )}
</div> </div>
<p className="text-[10px] text-slate-500 mt-2 font-mono uppercase tracking-widest leading-none"> <p className="text-[10px] text-slate-500 mt-2 font-mono uppercase tracking-widest leading-none">
{parseInt(timeLeft.d) > 0 ? "Until event" : "T-Minus to Horizon"} {isLive ? (
"Mission in Progress"
) : id === 'iss' ? (
"T-Minus to Horizon"
) : id === 'launch' ? (
"T-Minus to Ignition"
) : id === 'moon' ? (
"Until Lunar Phase"
) : (
"Days to Peak Phase"
)}
</p> </p>
</> </>
)} )}

View file

@ -12,10 +12,7 @@ const Starfield = dynamic(() => import('@/components/Starfield'), {
}); });
export default function MissionControl({ iss, moon }: MissionControlProps) { export default function MissionControl({ iss, moon, cosmic, launch }: MissionControlProps) {
const BASE_TIME = 1769610273000;
const events = useMemo(() => [ const events = useMemo(() => [
{ {
id: 'iss', id: 'iss',
@ -32,20 +29,20 @@ export default function MissionControl({ iss, moon }: MissionControlProps) {
icon: <Moon size={20} />, icon: <Moon size={20} />,
}, },
{ {
id: 'starlink', id: 'cosmic',
title: "Starlink Train", title: cosmic.title,
date: new Date(BASE_TIME + 1000 * 60 * 45), date: cosmic.start,
endDate: new Date(BASE_TIME + 1000 * 60 * 55), endDate: cosmic.end,
icon: <Rocket size={20} />, icon: <Sparkles size={20} />,
}, },
{ {
id: 'meteor', id: 'launch',
title: "Meteor Shower", title: launch.title,
date: new Date(BASE_TIME + 1000 * 60 * 60 * 24 * 10), date: launch.start,
endDate: new Date(BASE_TIME + 1000 * 60 * 60 * 24 * 10.1), endDate: launch.end,
icon: <Sparkles size={20} />, icon: <Rocket size={20} />,
} }
], [iss, moon]); ], [iss, moon, cosmic, launch]);
return ( return (
<div className="relative min-h-screen flex flex-col items-center p-8 overflow-hidden"> <div className="relative min-h-screen flex flex-col items-center p-8 overflow-hidden">
@ -63,7 +60,7 @@ export default function MissionControl({ iss, moon }: MissionControlProps) {
<div className="z-10 grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-5xl"> <div className="z-10 grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-5xl">
{events.map((event) => ( {events.map((event) => (
<EventCard <EventCard
key={`${event.id}-${event.date?.getTime()}`} key={`${event.id}-${event.date?.getTime() || 'none'}`}
id={event.id} id={event.id}
title={event.title} title={event.title}
targetDate={event.date} targetDate={event.date}

67
config/events.json Normal file
View file

@ -0,0 +1,67 @@
[
{
"id": "lunar_eclipse_mar_2026",
"title": "Total Lunar Eclipse",
"event_time": "2026-03-03T11:34:00Z"
},
{
"id": "solar_eclipse_aug_2026",
"title": "Solar Eclipse: Partial (85%)",
"event_time": "2026-08-12T17:10:00Z"
},
{
"id": "leonids_2026",
"title": "Leonid Meteor Shower",
"event_time": "2026-11-17T12:00:00Z"
},
{
"id": "solar_eclipse_aug_2027",
"title": "Solar Eclipse: Partial",
"event_time": "2027-08-02T10:05:00Z"
},
{
"id": "perseids_2027",
"title": "Perseid Meteor Shower",
"event_time": "2027-08-13T01:00:00Z"
},
{
"id": "lunar_eclipse_dec_2028",
"title": "Total Lunar Eclipse",
"event_time": "2028-12-31T16:53:00Z"
},
{
"id": "solar_eclipse_jun_2029",
"title": "Solar Eclipse: Partial",
"event_time": "2029-06-12T04:06:00Z"
},
{
"id": "lunar_eclipse_dec_2029",
"title": "Total Lunar Eclipse",
"event_time": "2029-12-20T22:42:00Z"
},
{
"id": "geminids_2030",
"title": "Geminid Meteor Shower",
"event_time": "2030-12-14T07:00:00Z"
},
{
"id": "solar_eclipse_nov_2032",
"title": "Solar Eclipse: Partial",
"event_time": "2032-11-03T05:34:00Z"
},
{
"id": "lunar_eclipse_apr_2033",
"title": "Total Lunar Eclipse",
"event_time": "2033-04-14T19:13:00Z"
},
{
"id": "solar_eclipse_mar_2034",
"title": "Solar Eclipse: Partial",
"event_time": "2034-03-20T10:18:00Z"
},
{
"id": "lunar_eclipse_feb_2035",
"title": "Total Lunar Eclipse",
"event_time": "2035-02-22T09:05:00Z"
}
]

View file

@ -1,5 +1,10 @@
import db from '../lib/db'; import db from '../lib/db';
import * as satellite from 'satellite.js'; import * as satellite from 'satellite.js';
import path from 'path';
import fs from 'fs';
import { CosmicEvent } from '@/types/space';
const MY_LAT = 55.6683; const MY_LAT = 55.6683;
const MY_LON = 12.5333; const MY_LON = 12.5333;
@ -75,44 +80,89 @@ async function updateISSData() {
console.log(`✅ Stored ${passesFound.length} future passes with start and end times.`); console.log(`✅ Stored ${passesFound.length} future passes with start and end times.`);
} }
// Add this to your worker script function updateMoonPhase() {
function getNextMoonPhase() { const LUNAR_CYCLE = 29.53059;
const LUNAR_CYCLE = 29.53059; // Days const KNOWN_NEW_MOON = new Date('2024-01-11T11:57:00Z');
const KNOWN_NEW_MOON = new Date('2024-01-11T11:57:00Z'); // A reference New Moon
const now = new Date(); const now = new Date();
const msSinceReference = now.getTime() - KNOWN_NEW_MOON.getTime(); const daysSince = (now.getTime() - KNOWN_NEW_MOON.getTime()) / 86400000;
const daysSinceReference = msSinceReference / (1000 * 60 * 60 * 24); const progress = daysSince % LUNAR_CYCLE;
const currentCycleProgress = daysSinceReference % LUNAR_CYCLE; const daysToFull = (LUNAR_CYCLE / 2) - progress;
const daysToFullMoon = (LUNAR_CYCLE / 2) - currentCycleProgress; const daysToNew = LUNAR_CYCLE - progress;
const daysToNewMoon = LUNAR_CYCLE - currentCycleProgress;
// If we've passed the Full Moon in this cycle, count to New Moon const moon = daysToFull > 0
let targetDate, title; ? { title: "Next Full Moon", date: new Date(now.getTime() + daysToFull * 86400000) }
if (daysToFullMoon > 0) { : { title: "Next New Moon", date: new Date(now.getTime() + daysToNew * 86400000) };
targetDate = new Date(now.getTime() + daysToFullMoon * 86400000);
title = "Next Full Moon"; db.prepare(`
} else { INSERT INTO global_events (id, title, event_time) VALUES ('moon_phase', ?, ?)
targetDate = new Date(now.getTime() + daysToNewMoon * 86400000); ON CONFLICT(id) DO UPDATE SET title=excluded.title, event_time=excluded.event_time
title = "Next New Moon"; `).run(moon.title, moon.date.toISOString());
console.log(`🌙 Moon: ${moon.title}`);
}
function updateCosmicEvents() {
const configPath = path.resolve(process.cwd(), 'config/events.json');
if (!fs.existsSync(configPath)) return;
const events: CosmicEvent[] = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const now = new Date();
const next = events
.filter((e) => new Date(e.event_time) > now)
.sort((a, b) => new Date(a.event_time).getTime() - new Date(b.event_time).getTime())[0];
if (next) {
db.prepare(`
INSERT INTO global_events (id, title, event_time) VALUES ('next_cosmic_event', ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, event_time=excluded.event_time
`).run(next.title, next.event_time);
console.log(`✨ Cosmic: ${next.title}`);
} }
}
return { title, targetDate }; async function updateRocketLaunches() {
try {
const nowISO = new Date().toISOString();
const url = `https://lldev.thespacedevs.com/2.2.0/launch/upcoming/?limit=1&mode=list&net__gt=${nowISO}`;
const response = await fetch(url);
const data = await response.json();
if (data.results && data.results.length > 0) {
const launch = data.results[0];
db.prepare(`
INSERT INTO global_events (id, title, event_time)
VALUES ('next_launch', ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, event_time=excluded.event_time
`).run(
`Launch: ${launch.name}`,
launch.net
);
console.log(`🚀 Rocket Sync: ${launch.name}`);
}
} catch (error) {
console.error("❌ Rocket Fetch Error:", error);
}
} }
async function updateAllData() { async function updateAllData() {
console.log("📡 Starting Ground Station sync...");
try {
await updateISSData(); await updateISSData();
updateMoonPhase();
updateCosmicEvents();
await updateRocketLaunches();
const moon = getNextMoonPhase(); console.log("✅ All systems synchronized.");
const upsertMoon = db.prepare(` } catch (error) {
INSERT INTO global_events (id, title, event_time) console.error("❌ Sync failed:", error);
VALUES ('moon_phase', ?, ?) }
ON CONFLICT(id) DO UPDATE SET title=excluded.title, event_time=excluded.event_time
`);
upsertMoon.run(moon.title, moon.targetDate.toISOString());
console.log(`🌙 Updated Moon Phase: ${moon.title}`);
} }
updateAllData().catch(console.error); updateAllData().catch(console.error);

View file

@ -32,4 +32,12 @@ export interface EventCardProps {
export interface MissionControlProps { export interface MissionControlProps {
iss: { start: Date | null; end: Date | null }; iss: { start: Date | null; end: Date | null };
moon: { title: string; start: Date | null; end: Date | null }; moon: { title: string; start: Date | null; end: Date | null };
cosmic: { title: string; start: Date | null; end: Date | null };
launch: { title: string; start: Date | null; end: Date | null };
}
export interface CosmicEvent {
id: string;
title: string;
event_time: string;
} }