diff --git a/.codex-temp/php-server.err.log b/.codex-temp/php-server.err.log index a469590..585c3e0 100644 --- a/.codex-temp/php-server.err.log +++ b/.codex-temp/php-server.err.log @@ -698,3 +698,87 @@ Stack trace: [Fri Apr 3 15:04:24 2026] 127.0.0.1:64920 Accepted [Fri Apr 3 15:04:26 2026] 127.0.0.1:64920 [200]: GET /favicon.ico [Fri Apr 3 15:04:26 2026] 127.0.0.1:64920 Closing +[Fri Apr 3 15:11:51 2026] 127.0.0.1:57308 Accepted +[Fri Apr 3 15:12:02 2026] 127.0.0.1:57308 [200]: GET /monatsvorhersage_old.php?year=2026&month=9 +[Fri Apr 3 15:12:02 2026] 127.0.0.1:57308 Closing +[Fri Apr 3 15:12:32 2026] 127.0.0.1:59860 Accepted +[Fri Apr 3 15:12:47 2026] 127.0.0.1:59860 [200]: GET /monatsvorhersage_old.php?year=2026&month=2 +[Fri Apr 3 15:12:47 2026] 127.0.0.1:59860 Closing +[Fri Apr 3 15:12:47 2026] 127.0.0.1:58680 Accepted +[Fri Apr 3 15:12:57 2026] 127.0.0.1:58680 [200]: GET /monatsvorhersage_old.php?year=2026&month=9 +[Fri Apr 3 15:12:57 2026] 127.0.0.1:58680 Closing +[Fri Apr 3 15:12:57 2026] 127.0.0.1:52748 Accepted +[Fri Apr 3 15:12:57 2026] 127.0.0.1:51924 Accepted +[Fri Apr 3 15:12:59 2026] 127.0.0.1:52748 [200]: GET /favicon.ico +[Fri Apr 3 15:12:59 2026] 127.0.0.1:52748 Closing +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64367 Accepted +[Fri Apr 3 15:12:59 2026] 127.0.0.1:51924 [200]: GET /css/style.css +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64367 [200]: GET /js/astronomy.browser.min.js +[Fri Apr 3 15:12:59 2026] 127.0.0.1:51924 Closing +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64367 Closing +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64871 Accepted +[Fri Apr 3 15:12:59 2026] 127.0.0.1:51645 Accepted +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64871 [200]: GET /images/star16x16.png +[Fri Apr 3 15:12:59 2026] 127.0.0.1:51645 [200]: GET /images/star16x16_rays.png +[Fri Apr 3 15:12:59 2026] 127.0.0.1:64871 Closing +[Fri Apr 3 15:12:59 2026] 127.0.0.1:51645 Closing +[Fri Apr 3 15:12:59 2026] 127.0.0.1:56330 Accepted +[Fri Apr 3 15:13:00 2026] 127.0.0.1:56330 [200]: GET /favicon.ico +[Fri Apr 3 15:13:00 2026] 127.0.0.1:56330 Closing +[Fri Apr 3 15:19:13 2026] 127.0.0.1:52242 Accepted +[Fri Apr 3 15:19:24 2026] 127.0.0.1:52242 [200]: GET /monatsvorhersage_old.php?year=2026&month=11 +[Fri Apr 3 15:19:24 2026] 127.0.0.1:52242 Closing +[Fri Apr 3 15:19:24 2026] 127.0.0.1:52243 Accepted +[Fri Apr 3 15:19:37 2026] 127.0.0.1:52243 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:19:37 2026] 127.0.0.1:52243 Closing +[Fri Apr 3 15:19:49 2026] 127.0.0.1:53174 Accepted +[Fri Apr 3 15:20:00 2026] 127.0.0.1:53174 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:20:00 2026] 127.0.0.1:53174 Closing +[Fri Apr 3 15:20:00 2026] 127.0.0.1:53176 Accepted +[Fri Apr 3 15:20:11 2026] 127.0.0.1:53176 [200]: GET /monatsvorhersage_old.php?year=2026&month=11 +[Fri Apr 3 15:20:11 2026] 127.0.0.1:53176 Closing +[Fri Apr 3 15:21:51 2026] 127.0.0.1:59375 Accepted +[Fri Apr 3 15:22:01 2026] 127.0.0.1:59375 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:22:01 2026] 127.0.0.1:59375 Closing +[Fri Apr 3 15:22:01 2026] 127.0.0.1:52790 Accepted +[Fri Apr 3 15:22:01 2026] 127.0.0.1:52790 [200]: GET /css/style.css +[Fri Apr 3 15:22:01 2026] 127.0.0.1:52790 Closing +[Fri Apr 3 15:22:01 2026] 127.0.0.1:64786 Accepted +[Fri Apr 3 15:22:01 2026] 127.0.0.1:64786 [200]: GET /js/astronomy.browser.min.js +[Fri Apr 3 15:22:01 2026] 127.0.0.1:64786 Closing +[Fri Apr 3 15:22:01 2026] 127.0.0.1:53818 Accepted +[Fri Apr 3 15:22:01 2026] 127.0.0.1:60750 Accepted +[Fri Apr 3 15:22:01 2026] 127.0.0.1:53818 [200]: GET /images/star16x16.png +[Fri Apr 3 15:22:01 2026] 127.0.0.1:60750 [200]: GET /images/star16x16_rays.png +[Fri Apr 3 15:22:01 2026] 127.0.0.1:53818 Closing +[Fri Apr 3 15:22:01 2026] 127.0.0.1:60750 Closing +[Fri Apr 3 15:22:01 2026] 127.0.0.1:60254 Accepted +[Fri Apr 3 15:22:03 2026] 127.0.0.1:60254 [200]: GET /favicon.ico +[Fri Apr 3 15:22:03 2026] 127.0.0.1:60254 Closing +[Fri Apr 3 15:23:32 2026] 127.0.0.1:54044 Accepted +[Fri Apr 3 15:23:42 2026] 127.0.0.1:54044 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:23:42 2026] 127.0.0.1:54044 Closing +[Fri Apr 3 15:24:14 2026] 127.0.0.1:54048 Accepted +[Fri Apr 3 15:24:22 2026] 127.0.0.1:54048 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:24:22 2026] 127.0.0.1:54048 Closing +[Fri Apr 3 15:24:51 2026] 127.0.0.1:65262 Accepted +[Fri Apr 3 15:24:59 2026] 127.0.0.1:65262 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:24:59 2026] 127.0.0.1:65262 Closing +[Fri Apr 3 15:39:18 2026] 127.0.0.1:62298 Accepted +[Fri Apr 3 15:39:27 2026] 127.0.0.1:62298 [200]: GET /monatsvorhersage_old.php?year=2026&month=10 +[Fri Apr 3 15:39:27 2026] 127.0.0.1:62298 Closing +[Fri Apr 3 15:39:27 2026] 127.0.0.1:50830 Accepted +[Fri Apr 3 15:39:27 2026] 127.0.0.1:50830 [200]: GET /css/style.css +[Fri Apr 3 15:39:27 2026] 127.0.0.1:50830 Closing +[Fri Apr 3 15:39:27 2026] 127.0.0.1:55999 Accepted +[Fri Apr 3 15:39:27 2026] 127.0.0.1:55999 [200]: GET /js/astronomy.browser.min.js +[Fri Apr 3 15:39:27 2026] 127.0.0.1:55999 Closing +[Fri Apr 3 15:39:27 2026] 127.0.0.1:56815 Accepted +[Fri Apr 3 15:39:27 2026] 127.0.0.1:59542 Accepted +[Fri Apr 3 15:39:27 2026] 127.0.0.1:56815 [200]: GET /images/star16x16.png +[Fri Apr 3 15:39:27 2026] 127.0.0.1:59542 [200]: GET /images/star16x16_rays.png +[Fri Apr 3 15:39:27 2026] 127.0.0.1:56815 Closing +[Fri Apr 3 15:39:27 2026] 127.0.0.1:59542 Closing +[Fri Apr 3 15:39:27 2026] 127.0.0.1:55947 Accepted +[Fri Apr 3 15:39:29 2026] 127.0.0.1:55947 [200]: GET /favicon.ico +[Fri Apr 3 15:39:29 2026] 127.0.0.1:55947 Closing diff --git a/public/monatsvorhersage_old.php b/public/monatsvorhersage_old.php index b233abe..a8822aa 100644 --- a/public/monatsvorhersage_old.php +++ b/public/monatsvorhersage_old.php @@ -57,6 +57,7 @@ $messierData = []; $planetData = []; $moonData = null; $moonPhaseEvents = []; +$goldenHandleEvents = []; $moonPlanetApproaches = []; $planetConjunctions = []; $eclipseEvents = []; @@ -337,6 +338,23 @@ if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) { $moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : []; } +$goldenHandleOutput = []; +$goldenHandleExitCode = 0; +$goldenHandleArgs = [ + $pythonScriptPath, + 'golden_handle_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$goldenHandleCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $goldenHandleArgs)) . ' 2>&1'; +exec($goldenHandleCommand, $goldenHandleOutput, $goldenHandleExitCode); +$goldenHandleRaw = trim(implode("\n", $goldenHandleOutput)); +$goldenHandleDecoded = json_decode($goldenHandleRaw, true); +if (is_array($goldenHandleDecoded) && ($goldenHandleDecoded['ok'] ?? false)) { + $goldenHandleEvents = is_array($goldenHandleDecoded['events'] ?? null) ? $goldenHandleDecoded['events'] : []; +} + $seasonOutput = []; $seasonExitCode = 0; $seasonArgs = [ @@ -597,6 +615,24 @@ foreach ($moonPhaseEvents as $phaseEvent) { ]; } +foreach ($goldenHandleEvents as $goldenHandleEvent) { + if (!is_array($goldenHandleEvent)) { + continue; + } + + $peakDate = (string) ($goldenHandleEvent['peak_local_date'] ?? ''); + $peakTime = (string) ($goldenHandleEvent['peak_local_time'] ?? ''); + $goldenHandleLabel = 'Goldener Henkel am Mond'; + + $monthEventList[] = [ + 'event' => $goldenHandleLabel, + 'date' => $peakDate !== '' ? $peakDate : (string) ($goldenHandleEvent['local_date'] ?? ''), + 'time' => $peakTime !== '' ? $peakTime : (string) ($goldenHandleEvent['local_time'] ?? ''), + 'local_iso' => (string) ($goldenHandleEvent['peak_local_iso'] ?? $goldenHandleEvent['local_iso'] ?? ''), + 'type' => 'golden_handle', + ]; +} + foreach ($seasonChanges as $seasonChange) { if (!is_array($seasonChange)) { continue; diff --git a/public/py/__pycache__/api.cpython-314.pyc b/public/py/__pycache__/api.cpython-314.pyc index 18f02cc..9a9d1fc 100644 Binary files a/public/py/__pycache__/api.cpython-314.pyc and b/public/py/__pycache__/api.cpython-314.pyc differ diff --git a/public/py/api.py b/public/py/api.py index d4acfb6..87f2f82 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -466,6 +466,130 @@ def action_moon_phases_for_month(args: list[str]) -> dict: } +def action_golden_handle_for_month(args: list[str]) -> dict: + if len(args) != 3: + fail( + "Aktion golden_handle_for_month erwartet 3 Argumente: year month timezone", + extra={"argv": args}, + ) + + try: + year = int(args[0]) + month = int(args[1]) + except ValueError as exc: + fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args}) + + if month < 1 or month > 12: + fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month}) + + timezone_name = args[2] + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz) + if month == 12: + local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz) + else: + local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz) + + # Approximate the Golden Handle geometrically by the effective selenographic + # colongitude over Montes Jura/Sinus Iridum (roughly 44 N, 36 W), + # corrected by libration. Empirically, the best-fit target shifts with + # libration in latitude by several tenths of a degree, i.e. by hours. + def target_colongitude(elat_deg: float) -> float: + return 33.385316705346064 + 0.11176238230626076 * elat_deg + 0.0734267792065295 * elat_deg * elat_deg + + def golden_handle_metric(dt_utc: datetime) -> float: + time_value = dt_to_time(dt_utc) + phase_deg = normalize_degrees(astronomy.MoonPhase(time_value)) + libration = astronomy.Libration(time_value) + effective_colongitude = normalize_degrees(phase_deg - 90.0 - float(libration.elon)) + return normalize_signed_degrees(effective_colongitude - target_colongitude(float(libration.elat))) + + def refine_peak_time(left_utc: datetime, right_utc: datetime) -> datetime: + left = left_utc + right = right_utc + f_left = golden_handle_metric(left) + f_right = golden_handle_metric(right) + for _ in range(40): + mid = left + (right - left) / 2 + f_mid = golden_handle_metric(mid) + if abs(f_mid) < 1e-5: + return mid + if f_left == 0: + return left + if f_right == 0: + return right + if f_left * f_mid <= 0: + right = mid + f_right = f_mid + else: + left = mid + f_left = f_mid + return left + (right - left) / 2 + + search_start = dt_to_time(local_start.astimezone(timezone.utc) - timedelta(days=20)) + events = [] + probe = search_start + for _ in range(3): + first_quarter = astronomy.SearchMoonPhase(90.0, probe, 40.0) + if first_quarter is None: + break + + full_moon = astronomy.SearchMoonPhase(180.0, first_quarter, 12.0) + if full_moon is None: + break + + interval_start = time_to_datetime(first_quarter) + interval_end = time_to_datetime(full_moon) + step = timedelta(hours=1) + previous_time = interval_start + previous_value = golden_handle_metric(previous_time) + current = interval_start + step + peak_utc = None + + while current <= interval_end: + current_value = golden_handle_metric(current) + if previous_value == 0 or current_value == 0 or previous_value * current_value < 0: + peak_utc = refine_peak_time(previous_time, current) + break + previous_time = current + previous_value = current_value + current += step + + if peak_utc is not None: + peak_local = peak_utc.astimezone(tz) + if peak_local.year == year and peak_local.month == month: + events.append({ + "kind": "golden_handle", + "label": "Goldener Henkel am Mond", + "utc_iso": peak_utc.isoformat().replace("+00:00", "Z"), + "local_iso": peak_local.isoformat(), + "local_date": peak_local.strftime("%d.%m.%Y"), + "local_time": peak_local.strftime("%H:%M"), + "peak_local_iso": peak_local.isoformat(), + "peak_local_date": peak_local.strftime("%d.%m.%Y"), + "peak_local_time": peak_local.strftime("%H:%M"), + }) + + probe = dt_to_time(interval_end + timedelta(days=10)) + + events.sort(key=lambda item: item["local_iso"]) + + return { + "ok": True, + "action": "golden_handle_for_month", + "selected": { + "year": year, + "month": month, + "timezone": timezone_name, + }, + "events": events, + } + + def action_season_changes_for_month(args: list[str]) -> dict: if len(args) != 3: fail( @@ -2578,7 +2702,7 @@ def action_satellite_passes(args: list[str]) -> dict: def main() -> None: if len(sys.argv) < 2: - fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "season_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -2623,6 +2747,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=False)) return + if action == "golden_handle_for_month": + result = action_golden_handle_for_month(args) + print(json.dumps(result, ensure_ascii=False)) + return + if action == "moon_planet_approaches": result = action_moon_planet_approaches(args) print(json.dumps(result, ensure_ascii=False)) @@ -2673,7 +2802,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=False)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "season_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) if __name__ == "__main__":