diff --git a/docs/README_DEV_LOGIN.md b/docs/README_DEV_LOGIN.md
new file mode 100644
index 0000000..2372d89
--- /dev/null
+++ b/docs/README_DEV_LOGIN.md
@@ -0,0 +1,59 @@
+# Lokaler Dev-Login
+
+Diese Projektkopie hat einen kleinen Entwicklungs-Bypass fuer den Login, damit geschuetzte Seiten auf `localhost` einfacher getestet werden koennen.
+
+## Zweck
+
+Der Dev-Login ist nur fuer die lokale Entwicklung gedacht.
+Er hilft beim Testen von Seiten wie:
+
+- `public/comets.php`
+- `public/my_favorites.php`
+- andere nicht oeffentlich freigegebene Seiten
+
+## Aktivierung
+
+Im lokalen Browser einfach eine geschuetzte Seite mit `?dev_login=1` aufrufen, zum Beispiel:
+
+```text
+http://localhost:8000/public/comets.php?dev_login=1
+```
+
+Dann wird automatisch der erste aktive Benutzer mit Rolle `master` oder `admin` in die Session eingeloggt.
+
+Danach koennen auch weitere geschuetzte Seiten im selben Browser/Fenster normal aufgerufen werden.
+
+## Abmelden
+
+Zum lokalen Dev-Logout:
+
+```text
+http://localhost:8000/public/comets.php?dev_logout=1
+```
+
+Das entfernt die Dev-Session wieder.
+
+## Sicherheitsgrenzen
+
+Der Dev-Login greift nur bei lokalen Requests:
+
+- Host muss `localhost`, `127.0.0.1` oder `::1` sein
+- der Request muss lokal kommen
+
+Auf dem echten Server oder unter normalen externen Domains hat dieser Mechanismus keine Wirkung.
+
+## Technische Umsetzung
+
+Die Erkennung und Session-Initialisierung liegt in:
+
+- [public/auth_helpers.php](/abs/path/c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php:4)
+
+Die Aktivierung beim Seitenaufbau liegt in:
+
+- [public/header.php](/abs/path/c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/header.php:3)
+
+## Hinweise
+
+- Der Dev-Login ist bewusst nicht fuer Produktion gedacht.
+- Wenn lokal kein aktiver `master`- oder `admin`-Benutzer vorhanden ist, greift der Bypass nicht.
+- Fuer reproduzierbare Tests am besten dieselbe Browser-Session weiterverwenden, statt staendig neu zu starten.
diff --git a/public/auth_helpers.php b/public/auth_helpers.php
index e3a79d1..0b40b97 100644
--- a/public/auth_helpers.php
+++ b/public/auth_helpers.php
@@ -1,6 +1,62 @@
PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
+ );
+
+ $stmt = $pdo->query("
+ SELECT `id`, `username`, `role`
+ FROM `app_users`
+ WHERE `is_active` = 1
+ AND `role` IN ('master', 'admin')
+ ORDER BY CASE WHEN `role` = 'master' THEN 0 ELSE 1 END, `id` ASC
+ LIMIT 1
+ ");
+ $user = $stmt->fetch();
+
+ if (is_array($user)) {
+ $_SESSION['user_id'] = (int) $user['id'];
+ $_SESSION['username'] = (string) $user['username'];
+ $_SESSION['role'] = (string) $user['role'];
+ }
+ } catch (Throwable $e) {
+ return;
+ }
+}
+
function currentUserRole(): string
{
return trim((string) ($_SESSION['role'] ?? ''));
diff --git a/public/comets.php b/public/comets.php
index ddd5cda..eae13a6 100644
--- a/public/comets.php
+++ b/public/comets.php
@@ -16,6 +16,81 @@ function h(?string $value): string
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
+function runPythonApi(string $action, array $args): array
+{
+ $scriptPath = __DIR__ . '/py/api.py';
+ $pythonCandidates = PHP_OS_FAMILY === 'Windows'
+ ? ['python', 'py']
+ : ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
+
+ $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
+ $lastError = 'Python konnte nicht gestartet werden.';
+
+ foreach ($pythonCandidates as $pythonBinary) {
+ $command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
+ $output = [];
+ $resultCode = 0;
+ exec($command, $output, $resultCode);
+
+ $joined = trim(implode("\n", $output));
+ $decoded = json_decode($joined, true);
+
+ if (is_array($decoded)) {
+ return [
+ 'ok' => (bool) ($decoded['ok'] ?? false),
+ 'data' => $decoded,
+ 'raw' => $joined,
+ 'command' => $command,
+ ];
+ }
+
+ if ($joined !== '') {
+ $lastError = $joined;
+ } elseif ($resultCode !== 0) {
+ $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
+ }
+ }
+
+ return [
+ 'ok' => false,
+ 'error' => $lastError,
+ ];
+}
+
+function runPythonApiWithJsonPayloadFile(string $action, array $payload): array
+{
+ $tempFile = tempnam(sys_get_temp_dir(), 'skyview_py_');
+ if ($tempFile === false) {
+ return [
+ 'ok' => false,
+ 'error' => 'Temporäre Datei für Python-Payload konnte nicht erstellt werden.',
+ ];
+ }
+
+ $jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ if ($jsonPayload === false) {
+ @unlink($tempFile);
+ return [
+ 'ok' => false,
+ 'error' => 'Python-Payload konnte nicht als JSON kodiert werden.',
+ ];
+ }
+
+ if (file_put_contents($tempFile, $jsonPayload) === false) {
+ @unlink($tempFile);
+ return [
+ 'ok' => false,
+ 'error' => 'Temporäre Datei für Python-Payload konnte nicht geschrieben werden.',
+ ];
+ }
+
+ try {
+ return runPythonApi($action, ['@' . $tempFile]);
+ } finally {
+ @unlink($tempFile);
+ }
+}
+
function formatDecimalGerman(?float $value, int $decimals = 3): string
{
if ($value === null || !is_finite($value)) {
@@ -25,6 +100,51 @@ function formatDecimalGerman(?float $value, int $decimals = 3): string
return number_format($value, $decimals, ',', '.');
}
+function formatRaHours(?float $hours): string
+{
+ if ($hours === null || !is_finite($hours)) {
+ return '-';
+ }
+
+ $normalized = fmod($hours, 24.0);
+ if ($normalized < 0.0) {
+ $normalized += 24.0;
+ }
+
+ $totalSeconds = (int) round($normalized * 3600.0);
+ $raHours = intdiv($totalSeconds, 3600) % 24;
+ $raMinutes = intdiv($totalSeconds % 3600, 60);
+ $raSeconds = $totalSeconds % 60;
+
+ return sprintf('%02dh %02dm %02ds', $raHours, $raMinutes, $raSeconds);
+}
+
+function formatDeclination(?float $degrees): string
+{
+ if ($degrees === null || !is_finite($degrees)) {
+ return '-';
+ }
+
+ $sign = $degrees < 0.0 ? '-' : '+';
+ $absolute = abs($degrees);
+ $deg = (int) floor($absolute);
+ $minutesFloat = ($absolute - $deg) * 60.0;
+ $minutes = (int) floor($minutesFloat);
+ $seconds = (int) round(($minutesFloat - $minutes) * 60.0);
+
+ if ($seconds >= 60) {
+ $seconds -= 60;
+ $minutes += 1;
+ }
+
+ if ($minutes >= 60) {
+ $minutes -= 60;
+ $deg += 1;
+ }
+
+ return sprintf('%s%02d° %02d\' %02d"', $sign, $deg, $minutes, $seconds);
+}
+
function buildPerihelionDate(array $row): ?DateTimeImmutable
{
$year = isset($row['year_of_perihelion']) ? (int) $row['year_of_perihelion'] : 0;
@@ -64,20 +184,6 @@ function classifyOrbitType(string $orbitType): string
};
}
-function estimatePerihelMagnitude(?float $absoluteMagnitudeH, ?float $perihelionDistanceAu): ?float
-{
- if ($absoluteMagnitudeH === null || !is_finite($absoluteMagnitudeH) || $perihelionDistanceAu === null || !is_finite($perihelionDistanceAu) || $perihelionDistanceAu <= 0.0) {
- return null;
- }
-
- $heliocentricDistance = max(0.05, $perihelionDistanceAu);
- $geocentricDistance = max(0.2, abs(1.0 - $perihelionDistanceAu));
-
- return $absoluteMagnitudeH
- + (5.0 * log10($geocentricDistance))
- + (10.0 * log10($heliocentricDistance));
-}
-
$config = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
@@ -121,36 +227,29 @@ $stmtComets->execute([
':year_min' => $queryYearMin,
':year_max' => $queryYearMax,
]);
+$cometRows = $stmtComets->fetchAll();
$allComets = [];
$trackedComets = [];
$interestingComets = [];
-foreach ($stmtComets as $row) {
+foreach ($cometRows as $row) {
$perihelionDate = buildPerihelionDate($row);
$daysFromToday = $perihelionDate ? (int) floor(($perihelionDate->getTimestamp() - $today->getTimestamp()) / 86400) : null;
$row['perihelion_date_object'] = $perihelionDate;
- $row['perihelion_date_display'] = $perihelionDate ? $perihelionDate->setTimezone(new DateTimeZone('Europe/Berlin'))->format('d.m.Y H:i') . ' UTC' : '-';
+ $row['perihelion_date_display'] = $perihelionDate ? $perihelionDate->format('d.m.Y H:i') . ' UTC' : '-';
$row['days_from_today'] = $daysFromToday;
$row['orbit_type_label'] = classifyOrbitType((string) ($row['orbit_type'] ?? ''));
$row['absolute_magnitude_display'] = isset($row['absolute_magnitude_h']) && $row['absolute_magnitude_h'] !== null
? formatDecimalGerman((float) $row['absolute_magnitude_h'], 1)
: '-';
- $row['slope_parameter_display'] = isset($row['slope_parameter_g']) && $row['slope_parameter_g'] !== null
+ $row['photometric_index_display'] = isset($row['slope_parameter_g']) && $row['slope_parameter_g'] !== null
? formatDecimalGerman((float) $row['slope_parameter_g'], 1)
: '-';
- $row['perihelion_dist_display'] = formatDecimalGerman(isset($row['perihelion_dist_au']) ? (float) $row['perihelion_dist_au'] : null, 3);
- $row['eccentricity_display'] = formatDecimalGerman(isset($row['eccentricity']) ? (float) $row['eccentricity'] : null, 4);
- $row['inclination_display'] = formatDecimalGerman(isset($row['inclination_deg']) ? (float) $row['inclination_deg'] : null, 2);
- $estimatedPerihelMagnitude = estimatePerihelMagnitude(
- isset($row['absolute_magnitude_h']) && $row['absolute_magnitude_h'] !== null ? (float) $row['absolute_magnitude_h'] : null,
- isset($row['perihelion_dist_au']) && $row['perihelion_dist_au'] !== null ? (float) $row['perihelion_dist_au'] : null
- );
- $row['estimated_perihel_magnitude'] = $estimatedPerihelMagnitude;
- $row['estimated_perihel_magnitude_display'] = $estimatedPerihelMagnitude !== null
- ? formatDecimalGerman($estimatedPerihelMagnitude, 1)
- : '-';
+ $row['estimated_magnitude'] = null;
+ $row['estimated_magnitude_display'] = '-';
+ $row['brightness_error'] = null;
$allComets[] = $row;
@@ -183,6 +282,94 @@ usort($interestingComets, static function (array $left, array $right): int {
});
$interestingComets = array_slice($interestingComets, 0, 24);
+$visibleCometIds = [];
+foreach (array_merge($trackedComets, $interestingComets) as $visibleComet) {
+ $visibleId = isset($visibleComet['id']) ? (int) $visibleComet['id'] : 0;
+ if ($visibleId > 0) {
+ $visibleCometIds[$visibleId] = true;
+ }
+}
+
+$calculationMoment = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+$brightnessByCometId = [];
+$brightnessError = null;
+
+if ($visibleCometIds !== []) {
+ $pythonPayload = [
+ 'date' => $calculationMoment->format('Y-m-d\TH:i:s\Z'),
+ 'comets' => array_values(array_map(static function (array $row): array {
+ return [
+ 'id' => $row['id'] ?? null,
+ 'year_of_perihelion' => $row['year_of_perihelion'] ?? null,
+ 'month_of_perihelion' => $row['month_of_perihelion'] ?? null,
+ 'day_of_perihelion' => $row['day_of_perihelion'] ?? null,
+ 'perihelion_dist_au' => $row['perihelion_dist_au'] ?? null,
+ 'eccentricity' => $row['eccentricity'] ?? null,
+ 'arg_perihelion_deg' => $row['arg_perihelion_deg'] ?? null,
+ 'ascending_node_deg' => $row['ascending_node_deg'] ?? null,
+ 'inclination_deg' => $row['inclination_deg'] ?? null,
+ 'absolute_magnitude_h' => $row['absolute_magnitude_h'] ?? null,
+ 'slope_parameter_g' => $row['slope_parameter_g'] ?? null,
+ ];
+ }, array_filter($cometRows, static function (array $row) use ($visibleCometIds): bool {
+ $rowId = isset($row['id']) ? (int) $row['id'] : 0;
+ return $rowId > 0 && isset($visibleCometIds[$rowId]);
+ }))),
+ ];
+
+ $pythonResult = runPythonApiWithJsonPayloadFile('comet_brightnesses', $pythonPayload);
+
+ if (($pythonResult['ok'] ?? false) && isset($pythonResult['data']['results']) && is_array($pythonResult['data']['results'])) {
+ foreach ($pythonResult['data']['results'] as $result) {
+ $resultId = isset($result['id']) ? (int) $result['id'] : 0;
+ if ($resultId > 0) {
+ $brightnessByCometId[$resultId] = $result;
+ }
+ }
+ } else {
+ $brightnessError = (string) ($pythonResult['error'] ?? 'Python-Berechnung nicht verfuegbar.');
+ }
+}
+
+$applyBrightness = static function (array $row) use ($brightnessByCometId): array {
+ $brightnessData = isset($row['id']) ? ($brightnessByCometId[(int) $row['id']] ?? null) : null;
+ $estimatedMagnitude = isset($brightnessData['estimated_magnitude']) && is_numeric($brightnessData['estimated_magnitude'])
+ ? (float) $brightnessData['estimated_magnitude']
+ : null;
+ $raHours = isset($brightnessData['ra_hours']) && is_numeric($brightnessData['ra_hours'])
+ ? (float) $brightnessData['ra_hours']
+ : null;
+ $decDeg = isset($brightnessData['dec_deg']) && is_numeric($brightnessData['dec_deg'])
+ ? (float) $brightnessData['dec_deg']
+ : null;
+ $heliocentricDistance = isset($brightnessData['heliocentric_distance_au']) && is_numeric($brightnessData['heliocentric_distance_au'])
+ ? (float) $brightnessData['heliocentric_distance_au']
+ : null;
+ $geocentricDistance = isset($brightnessData['geocentric_distance_au']) && is_numeric($brightnessData['geocentric_distance_au'])
+ ? (float) $brightnessData['geocentric_distance_au']
+ : null;
+
+ $row['estimated_magnitude'] = $estimatedMagnitude;
+ $row['estimated_magnitude_display'] = $estimatedMagnitude !== null
+ ? formatDecimalGerman($estimatedMagnitude, 1)
+ : '-';
+ $row['ra_hours'] = $raHours;
+ $row['ra_display'] = formatRaHours($raHours);
+ $row['dec_deg'] = $decDeg;
+ $row['dec_display'] = formatDeclination($decDeg);
+ $row['heliocentric_distance_au'] = $heliocentricDistance;
+ $row['heliocentric_distance_display'] = formatDecimalGerman($heliocentricDistance, 3);
+ $row['geocentric_distance_au'] = $geocentricDistance;
+ $row['geocentric_distance_display'] = formatDecimalGerman($geocentricDistance, 3);
+ $row['brightness_error'] = is_array($brightnessData) && !empty($brightnessData['error'])
+ ? (string) $brightnessData['error']
+ : null;
+
+ return $row;
+};
+
+$trackedComets = array_map($applyBrightness, $trackedComets);
+$interestingComets = array_map($applyBrightness, $interestingComets);
$orbitTypeCounts = [];
foreach ($allComets as $comet) {
$orbitKey = (string) ($comet['orbit_type'] ?? '?');
@@ -365,9 +552,15 @@ arsort($orbitTypeCounts);
Nutzer-Favoriten aus app_user_comets werden zusaetzlich separat gezeigt.
- Die Spalte m grob ist eine sehr einfache Helligkeitsschaetzung nahe dem Perihel
- auf Basis von MPC-H und q. Sie ist nur als Orientierung gedacht.
+ Die Spalte m grob nutzt jetzt die uebliche MPC-Kometenformel fuer heute:
+ H + 5 log(Delta) + 2,5 * n * log(r). Sie bleibt trotzdem nur eine Orientierung.
+ Position und Helligkeit werden fuer = h($calculationMoment->format('d.m.Y H:i')) ?> UTC berechnet. +
+ +Die Python-Berechnung der Helligkeit war nicht verfuegbar: = h($brightnessError) ?>
+