diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e7e1a8d..28489b3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,33 @@ "Bash(php --version)", "Bash(/c/Users/ich/Downloads/php/php -S localhost:8080 -t public)", "Bash(/c/Users/ich/Downloads/php/php -m)", - "Bash(/c/Users/ich/Downloads/php/php --ini)" + "Bash(/c/Users/ich/Downloads/php/php --ini)", + "Bash(php -S localhost:8080 -t c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public)", + "Bash(where php:*)", + "Bash(/e/php/php.exe -S localhost:8080 -t \"c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public\")", + "Bash(/e/php/php.exe -m)", + "Read(//e/php/ext//**)", + "Bash(/e/php/php.exe --ini)", + "Bash(sed -i 's|^;extension_dir = \"ext\"|extension_dir = \"ext\"|' /e/php/php.ini)", + "Bash(sed -i 's|^;extension=pdo_mysql|extension=pdo_mysql|' /e/php/php.ini)", + "Bash(sed -i 's|^;extension=mysqli|extension=mysqli|' /e/php/php.ini)", + "Bash(taskkill //F //IM php.exe)", + "Bash(/usr/bin/python3 --version)", + "Bash(/usr/bin/python --version)", + "Read(//c/Users/HP/AppData/Local/Programs/**)", + "Bash(ls /c/Python*/python.exe)", + "Bash(/c/Users/HP/AppData/Local/Python/bin/python.exe --version)", + "Bash(/c/Users/HP/AppData/Local/Python/bin/python.exe c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/py/api.py sun_moon_rise_set 50.35 11.17 360 2026-04-01 Europe/Berlin)", + "Bash(/c/Users/HP/AppData/Local/Python/bin/python.exe -c \"from zoneinfo import ZoneInfo; ZoneInfo\\('Europe/Berlin'\\)\")", + "Bash(/c/Users/HP/AppData/Local/Python/bin/python.exe -m pip install tzdata)", + "Bash(/e/php/php.exe -r ':*)", + "Bash(/e/php/php.exe -r \"echo ini_get\\('disable_functions'\\);\")", + "Bash(/e/php/php.exe -S localhost:8080 -r \"echo ini_get\\('disable_functions'\\);\")", + "Bash(cp /tmp/debug_exec.php \"c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/debug_exec.php\")", + "Bash(/e/php/php.exe -l \"c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/index.php\")", + "Bash(/e/php/php.exe \"c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/index.php\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8080/index.php)", + "Bash(curl -s http://localhost:8080/index.php)" ] } } diff --git a/public/css/style.css b/public/css/style.css index 519b337..8440eaa 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -828,6 +828,58 @@ footer a:hover { color: var(--gold); } padding-right: 1rem; } +/* Kompakte Auf-/Untergangs-Übersicht (Startseite) */ +.riseset-card { display: inline-block; } + +.riseset-grid { + display: grid; + grid-template-columns: auto auto auto; + column-gap: 1.2rem; + row-gap: 0.18rem; + align-items: baseline; + font-size: 0.88rem; +} + +.riseset-label { color: var(--text-dim); white-space: nowrap; } +.riseset-val { font-variant-numeric: tabular-nums; white-space: nowrap; } + +.riseset-main.riseset-label, +.riseset-main.riseset-val { + color: var(--gold-pale); + font-weight: 600; + font-size: 0.92rem; +} + +.riseset-moon.riseset-label, +.riseset-moon.riseset-val { + color: var(--cyan); + font-weight: 600; + font-size: 0.92rem; +} + +.riseset-dim.riseset-label, +.riseset-dim.riseset-val { + color: var(--text-dim); + font-size: 0.8rem; +} + +.riseset-sep { + grid-column: 1 / -1; + height: 0; + border-top: 1px solid var(--border); + margin: 0.3rem 0; +} + +.sat-passes-grid { + grid-template-columns: auto auto auto; +} + +.sat-passes-grid small { + color: var(--text-dim); + font-size: 0.75em; + margin-left: 0.2em; +} + /* --- Aktions-Zelle (Buttons in Tabellenzeilen) ------------ */ .action-cell { diff --git a/public/index.php b/public/index.php index 0a4c27e..de1adb7 100644 --- a/public/index.php +++ b/public/index.php @@ -1,58 +1,236 @@ PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ] - ); - - $userId = (int)$_SESSION['user_id']; - $displayName = $_SESSION['display_name'] ?? ''; - $username = $_SESSION['username'] ?? ''; - $role = $_SESSION['role'] ?? 'user'; - - $stmtLocation = $pdo->prepare(" - SELECT - `name`, - `latitude`, - `longitude`, - `elevation`, - `timezone` - FROM `app_user_locations` - WHERE `user_id` = :user_id - AND `is_default` = 1 - LIMIT 1 - "); - $stmtLocation->execute([':user_id' => $userId]); - $defaultLocation = $stmtLocation->fetch(); +if (!function_exists('h')) { + function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } +} +if (!function_exists('risesetTime')) { + function risesetTime(array $ev, string $key): string { return (!empty($ev[$key]['found'])) ? htmlspecialchars($ev[$key]['local_time'], ENT_QUOTES, 'UTF-8') : '—'; } +} +if (!function_exists('fmtLocalTime')) { + function fmtLocalTime(string $utcIso, string $tz): string { return (new DateTime($utcIso))->setTimezone(new DateTimeZone($tz))->format('H:i'); } +} +if (!function_exists('azDir')) { + function azDir(float $az): string { $dirs = ['N','NO','O','SO','S','SW','W','NW']; return $dirs[(int)round($az / 45) % 8]; } } -function h(?string $value): string +function idx_tleChecksum(string $line): int { - return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + $sum = 0; + for ($i = 0; $i < 68; $i++) { + $c = $line[$i]; + if (ctype_digit($c)) $sum += (int)$c; + elseif ($c === '-') $sum += 1; + } + return $sum % 10; +} +function idx_tleExp(float $val): string +{ + if (abs($val) < 1e-99) return ' 00000-0'; + $sign = $val >= 0 ? '+' : '-'; + $abs = abs($val); + $exp = (int)floor(log10($abs)) + 1; + $mantissa = (int)round($abs / pow(10, $exp) * 1e5); + if ($mantissa >= 100000) { $mantissa = 10000; $exp++; } + return sprintf('%s%05d%s%d', $sign, $mantissa, $exp >= 0 ? '+' : '-', abs($exp)); +} +function idx_tleMmDot(float $val): string +{ + return sprintf('%s.%08d', $val >= 0 ? ' ' : '-', (int)round(abs($val) * 1e8)); +} +function idx_tleEpoch(string $epochStr): string +{ + $dt = new DateTimeImmutable($epochStr, new DateTimeZone('UTC')); + $yy = (int)$dt->format('Y') % 100; + $jan1 = new DateTimeImmutable($dt->format('Y') . '-01-01 00:00:00', new DateTimeZone('UTC')); + $day = ((float)$dt->format('U') - (float)$jan1->format('U')) / 86400.0 + 1.0; + return sprintf('%02d%012.8f', $yy, $day); +} +function idx_tleIntlDesig(?string $id): string +{ + if (!$id) return str_repeat(' ', 8); + if (preg_match('/^(\d{4})-(\d+)([A-Z]*)$/i', trim($id), $m)) { + $yy = substr($m[1], 2, 2); + return substr(str_pad($yy . str_pad($m[2], 3, '0', STR_PAD_LEFT) . str_pad(strtoupper($m[3]), 3), 8), 0, 8); + } + return str_pad(substr($id, 0, 8), 8); +} +function buildTleLine1(array $sat, array $tle): string +{ + $line = sprintf('1 %05d%1s %-8s %14s %10s %8s %8s %1d %4d', + (int)$sat['norad_cat_id'], + in_array($sat['classification_type'] ?? '', ['U','C','S'], true) ? $sat['classification_type'] : 'U', + idx_tleIntlDesig($sat['object_id'] ?? null), + idx_tleEpoch((string)$tle['epoch']), + idx_tleMmDot((float)($tle['mean_motion_dot'] ?? 0)), + idx_tleExp((float)($tle['mean_motion_ddot'] ?? 0)), + idx_tleExp((float)($tle['bstar'] ?? 0)), + (int)($tle['ephemeris_type'] ?? 0), + (int)($tle['element_set_no'] ?? 0) + ); + $line = str_pad($line, 68); + return $line . idx_tleChecksum($line); +} +function buildTleLine2(array $sat, array $tle): string +{ + $ecc = (float)($tle['eccentricity'] ?? 0); + $line = sprintf('2 %05d %8.4f %8.4f %7s %8.4f %8.4f %11.8f%5d', + (int)$sat['norad_cat_id'], + (float)($tle['inclination'] ?? 0), + (float)($tle['ra_of_asc_node'] ?? 0), + substr(sprintf('%.7f', $ecc), 2), + (float)($tle['arg_of_pericenter'] ?? 0), + (float)($tle['mean_anomaly'] ?? 0), + (float)($tle['mean_motion'] ?? 0), + (int)($tle['rev_at_epoch'] ?? 0) + ); + $line = str_pad($line, 68); + return $line . idx_tleChecksum($line); +} + +// Standort und astronomische Daten nur wenn eingeloggt und Standort vorhanden +$skyData = null; +$planetData = null; +$satData = null; +$pyError = null; + +if ($loggedIn) { + // header.php lädt $currentLocation – wir brauchen ihn vorab + $config = require __DIR__ . '/../config/database.php'; + $pdo = new PDO( + sprintf('mysql:host=%s;dbname=%s;charset=%s', $config['host'], $config['dbname'], $config['charset'] ?? 'utf8mb4'), + $config['user'], + $config['pass'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] + ); + + $stmtLoc = $pdo->prepare( + 'SELECT name, latitude, longitude, elevation, timezone + FROM app_user_locations WHERE user_id = ? AND is_default = 1 LIMIT 1' + ); + $stmtLoc->execute([(int)$_SESSION['user_id']]); + $defaultLocation = $stmtLoc->fetch() ?: null; + + if ($defaultLocation) { + $lat = (float)$defaultLocation['latitude']; + $lon = (float)$defaultLocation['longitude']; + $elev = (float)($defaultLocation['elevation'] ?? 0); + $tz = $defaultLocation['timezone'] ?? 'Europe/Berlin'; + $date = date('Y-m-d'); + + $scriptPath = realpath(__DIR__ . '/py/api.py'); + if (file_exists('/usr/bin/python3')) { + $pythonBin = '/usr/bin/python3'; + } elseif (file_exists('/usr/bin/python')) { + $pythonBin = '/usr/bin/python'; + } else { + $pythonBin = 'C:\\Users\\HP\\AppData\\Local\\Python\\bin\\python.exe'; + } + putenv('PYTHONUTF8=1'); + putenv('PYTHONIOENCODING=utf-8'); + + // Sonne + Mond + $args = [ + $pythonBin, + escapeshellarg($scriptPath), + escapeshellarg('sun_moon_rise_set'), + escapeshellarg((string)$lat), + escapeshellarg((string)$lon), + escapeshellarg((string)$elev), + escapeshellarg($date), + escapeshellarg($tz), + ]; + $cmd = implode(' ', $args) . ' 2>&1'; + $out = []; + exec($cmd, $out); + $raw = json_decode(implode('', $out), true); + if (is_array($raw) && ($raw['ok'] ?? false)) { + $skyData = $raw; + } else { + $pyError = $raw['error'] ?? 'Fehler beim Berechnen der Aufgangszeiten.'; + } + + // Planeten + $args2 = [ + $pythonBin, + escapeshellarg($scriptPath), + escapeshellarg('planet_rise_set'), + escapeshellarg((string)$lat), + escapeshellarg((string)$lon), + escapeshellarg((string)$elev), + escapeshellarg($date), + escapeshellarg($tz), + ]; + $cmd2 = implode(' ', $args2) . ' 2>&1'; + $out2 = []; + exec($cmd2, $out2); + $raw2 = json_decode(implode('', $out2), true); + if (is_array($raw2) && ($raw2['ok'] ?? false)) { + $planetData = $raw2; + } elseif ($pyError === null) { + $pyError = $raw2['error'] ?? 'Fehler beim Berechnen der Planetenzeiten.'; + } + + // Satelliten-Überflüge heute — Favorit zuerst, dann alle gemerkten + $stmtSat = $pdo->prepare( + 'SELECT s.id, s.norad_cat_id, s.object_name, s.object_type, + s.object_id, s.classification_type, + t.epoch, t.mean_motion, t.eccentricity, t.inclination, + t.ra_of_asc_node, t.arg_of_pericenter, t.mean_anomaly, + t.ephemeris_type, t.element_set_no, t.rev_at_epoch, + t.bstar, t.mean_motion_dot, t.mean_motion_ddot, + us.is_favorite + FROM app_user_satellites us + JOIN sat_satellites s ON s.id = us.satellite_id + JOIN sat_tle_current t ON t.satellite_id = s.id + WHERE us.user_id = ? + AND t.epoch IS NOT NULL + ORDER BY us.is_favorite DESC, s.object_name ASC + LIMIT 3' + ); + $stmtSat->execute([(int)$_SESSION['user_id']]); + $userSats = $stmtSat->fetchAll(); + + if ($userSats) { + $tzObj = new DateTimeZone($tz); + $dayStart = new DateTime('today', $tzObj); + $dayStart->setTimezone(new DateTimeZone('UTC')); + $dayEnd = new DateTime('tomorrow', $tzObj); + $dayEnd->setTimezone(new DateTimeZone('UTC')); + $satsJson = json_encode(array_map(fn($r) => [ + 'id' => $r['id'], + 'norad_cat_id' => $r['norad_cat_id'], + 'object_name' => $r['object_name'], + 'tle_line1' => buildTleLine1($r, $r), + 'tle_line2' => buildTleLine2($r, $r), + 'is_favorite' => (bool)$r['is_favorite'], + ], $userSats), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $args3 = [ + $pythonBin, + escapeshellarg($scriptPath), + escapeshellarg('satellite_passes'), + escapeshellarg((string)$lat), + escapeshellarg((string)$lon), + escapeshellarg((string)$elev), + escapeshellarg($tz), + escapeshellarg($dayStart->format('Y-m-d\TH:i:s\Z')), + escapeshellarg($dayEnd->format('Y-m-d\TH:i:s\Z')), + escapeshellarg($satsJson), + ]; + $cmd3 = implode(' ', $args3) . ' 2>&1'; + $out3 = []; + exec($cmd3, $out3); + $raw3 = json_decode(implode('', $out3), true); + if (is_array($raw3) && ($raw3['ok'] ?? false)) { + $satData = $raw3; + } + } + } } ?> @@ -70,36 +248,87 @@ function h(?string $value): string -

- Willkommen zurück, . -

+ +

Noch kein Standort hinterlegt. Jetzt anlegen

+ + + -
+

+ Aufgangs- und Untergangszeiten für + + — +

-
-

Konto

- - - -
Benutzername
Rolle
+ +
+

Heute

+
+ ☀ Sonne + ↑ + ↓ + + Bürgerl. + + + + Nautisch + + + + Astronom. + + + + + + ☽ Mond + ↑ + ↓ + + + + + + ↑ + ↓ + +
-
-

Standard-Standort

- - - - - - - -
Name
Breitengrad
Längengrad
Höhe
Zeitzone
+ + strcmp($a['aos_utc'], $b['aos_utc'])); + ?> +
+

☆

+ +

Heute keine Überflüge.

-

Noch kein Standort hinterlegt. Jetzt anlegen

+
+ AOS + LOS + Max + + + + ° + +
+ -
+ diff --git a/public/py/__pycache__/astronomy.cpython-314.pyc b/public/py/__pycache__/astronomy.cpython-314.pyc new file mode 100644 index 0000000..d9f01f5 Binary files /dev/null and b/public/py/__pycache__/astronomy.cpython-314.pyc differ diff --git a/public/py/api.py b/public/py/api.py index 4f28786..71d9b19 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -151,6 +151,24 @@ def action_sun_moon_rise_set(args: list[str]) -> dict: moon_rise = search_event(astronomy.Body.Moon, astronomy.Direction.Rise, observer, start_time, utc_end) moon_set = search_event(astronomy.Body.Moon, astronomy.Direction.Set, observer, start_time, utc_end) + def search_altitude(direction: astronomy.Direction, altitude_deg: float) -> astronomy.Time | None: + limit_days = (utc_end - time_to_datetime(start_time)).total_seconds() / 86400.0 + result = astronomy.SearchAltitude(astronomy.Body.Sun, observer, direction, start_time, limit_days, altitude_deg) + if result is None: + return None + if time_to_datetime(result) >= utc_end: + return None + return result + + # Morgens: Sonne steigt auf → Direction.Rise (Höhe wird größer) + # Abends: Sonne sinkt ab → Direction.Set (Höhe wird kleiner) + astro_dawn = search_altitude(astronomy.Direction.Rise, -18.0) + nautical_dawn = search_altitude(astronomy.Direction.Rise, -12.0) + civil_dawn = search_altitude(astronomy.Direction.Rise, -6.0) + civil_dusk = search_altitude(astronomy.Direction.Set, -6.0) + nautical_dusk = search_altitude(astronomy.Direction.Set, -12.0) + astro_dusk = search_altitude(astronomy.Direction.Set, -18.0) + return { "ok": True, "action": "sun_moon_rise_set", @@ -168,14 +186,81 @@ def action_sun_moon_rise_set(args: list[str]) -> dict: "utc_end": utc_end.isoformat().replace("+00:00", "Z"), }, "events": { - "sunrise": serialize_event("Sonnenaufgang", sun_rise, tz), - "sunset": serialize_event("Sonnenuntergang", sun_set, tz), - "moonrise": serialize_event("Mondaufgang", moon_rise, tz), - "moonset": serialize_event("Monduntergang", moon_set, tz), + "astro_dawn": serialize_event("Astronomische Morgendämmerung", astro_dawn, tz), + "nautical_dawn": serialize_event("Nautische Morgendämmerung", nautical_dawn, tz), + "civil_dawn": serialize_event("Bürgerliche Morgendämmerung", civil_dawn, tz), + "sunrise": serialize_event("Sonnenaufgang", sun_rise, tz), + "sunset": serialize_event("Sonnenuntergang", sun_set, tz), + "civil_dusk": serialize_event("Bürgerliche Abenddämmerung", civil_dusk, tz), + "nautical_dusk": serialize_event("Nautische Abenddämmerung", nautical_dusk, tz), + "astro_dusk": serialize_event("Astronomische Abenddämmerung", astro_dusk, tz), + "moonrise": serialize_event("Mondaufgang", moon_rise, tz), + "moonset": serialize_event("Monduntergang", moon_set, tz), }, } +PLANETS = [ + ("Merkur", astronomy.Body.Mercury), + ("Venus", astronomy.Body.Venus), + ("Mars", astronomy.Body.Mars), + ("Jupiter", astronomy.Body.Jupiter), + ("Saturn", astronomy.Body.Saturn), + ("Uranus", astronomy.Body.Uranus), + ("Neptun", astronomy.Body.Neptune), +] + + +def action_planet_rise_set(args: list[str]) -> dict: + if len(args) != 5: + fail( + "Aktion planet_rise_set erwartet 5 Argumente: latitude longitude elevation date timezone", + extra={"argv": args}, + ) + + latitude = parse_float(args[0], "Latitude") + longitude = parse_float(args[1], "Longitude") + elevation = parse_float(args[2], "Elevation") + date_text = args[3] + timezone_name = args[4] + + try: + datetime.strptime(date_text, "%Y-%m-%d") + except ValueError: + fail("Datum muss im Format YYYY-MM-DD uebergeben werden.", extra={"date": date_text}) + + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: + fail("Zeitzone ist ungueltig.", extra={"details": str(exc)}) + + observer = astronomy.Observer(latitude, longitude, elevation) + + local_start = datetime.strptime(date_text, "%Y-%m-%d").replace(tzinfo=tz) + local_end = local_start + timedelta(days=1) + utc_start = local_start.astimezone(timezone.utc) + utc_end = local_end.astimezone(timezone.utc) + start_time = dt_to_time(utc_start) + + planets = [] + for name, body in PLANETS: + rise = search_event(body, astronomy.Direction.Rise, observer, start_time, utc_end) + set_ = search_event(body, astronomy.Direction.Set, observer, start_time, utc_end) + planets.append({ + "name": name, + "rise": serialize_event(name + "aufgang", rise, tz), + "set": serialize_event(name + "untergang", set_, tz), + }) + + return { + "ok": True, + "action": "planet_rise_set", + "date": date_text, + "timezone": timezone_name, + "planets": planets, + } + + def classify_visibility(sun_alt_deg: float) -> dict: if sun_alt_deg > -5: return {"key": "day", "label": "Tag"} @@ -1201,7 +1286,12 @@ 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"]}) + if action == "planet_rise_set": + result = action_planet_rise_set(args) + 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"]}) if __name__ == "__main__":