Files
breadcrumb-the-shire/docs/geplante-aufgaben.md
fs 25370a1a55 add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00

7.2 KiB
Raw Blame History

Geplante Aufgaben

Letzte Aktualisierung: 2026-02-21

Ziel

Zentrale Verwaltung geplanter Aufgaben im Admin-Bereich.

  • Ein zentraler Runner (bin/scheduler-run.php) läuft minütlich.
    • Produktion typischerweise per Cron.
    • Lokale Docker-Umgebung über den Compose-Service scheduler.
  • Die konkreten Jobs sind Whitelist-basiert (kein frei ausführbarer Bash-Text aus DB).
  • V1 startet mit user_lifecycle_run.

Tabellen

  • scheduled_jobs
    • Konfiguration pro Job (enabled, schedule, timezone, next/last run, letzter Fehler).
  • scheduled_job_runs
    • Historie einzelner Runs (trigger, status, Dauer, error/result summary).
  • scheduler_runtime_status
    • Einzeilige Runtime-Health des globalen Runners (Heartbeat, letzter Runner-Status, letzter Fehlercode).

Run-Log-Retention:

  • 90 Tage (Purge in Admin verfügbar).

Rechte

  • jobs.view: Bereich ansehen
  • jobs.manage: Job-Konfiguration speichern
  • jobs.run_now: Job manuell ausführen

Purge der Run-Logs bleibt unter settings.update.

Betriebsmodell

Cron-Eintrag (Beispiel):

* * * * * docker compose exec php php bin/scheduler-run.php

Der Runner:

  1. sichert sich globalen Lock (GET_LOCK('scheduled_jobs_runner', 0)) nur ein Runner aktiv
  2. holt globale Due-Jobs (enabled=1, next_run_at <= now)
  3. führt Jobs kontrolliert aus
  4. schreibt Heartbeat in scheduler_runtime_status
  5. schreibt Run-Log und aktualisiert Job-Status
  6. berechnet next_run_at neu
  7. gibt den Lock frei

Runner-Heartbeat

  • scheduler_runtime_status enthält genau eine Zeile (id = 1).
  • Jeder Aufruf von bin/scheduler-run.php aktualisiert diese Zeile:
    • last_heartbeat_at
    • last_result (ok, lock_not_acquired, unexpected_error)
    • last_error_code (optional)
  • Dadurch wächst die Tabelle nicht mit der Zeit; sie ist ein laufendes Statusfenster.

Verwendung in Admin/Stats:

  • Kachel Cron runner active ist Aktiv, wenn last_heartbeat_at jünger als 3 Minuten ist.
  • Ist der Heartbeat älter oder fehlt, zeigt die Kachel Inaktiv.
  • Manuelle Runs aus der UI (Run now) zählen nicht als Cron-Liveness.

Scheduling (V1)

Unterstützte Typen:

  • hourly (Intervall 1..24)
  • daily (Intervall 1..365 + Uhrzeit)
  • weekly (Intervall 1..52 + Uhrzeit + Wochentage)

catchup_once:

  • Wenn ein Lauf verpasst wurde, wird genau ein Run nachgeholt.
  • Danach wird normal in die Zukunft weitergeplant (kein Backlog-Sturm).

Neuen Job registrieren (Entwickler-Guide)

Architektur-überblick

Jeder Job ist eine eigene Handler-Klasse, die ScheduledJobHandlerInterface implementiert. Die Registry ist eine reine Map von job_key zu Handler-Klasse sie enthält keine job-spezifische Logik.

lib/Service/Scheduler/
  Handler/
    ScheduledJobHandlerInterface.php   ← Vertrag für alle Handler
    UserLifecycleJobHandler.php        ← Referenz-Implementierung
  ScheduledJobRegistry.php            ← Handler-Map (einzige Datei die bei neuen Jobs angefasst wird)

Schritt-für-Schritt

1. Handler-Klasse erstellen

Datei: lib/Service/Scheduler/Handler/MeinJobHandler.php

<?php

namespace MintyPHP\Service\Scheduler\Handler;

use MintyPHP\Service\MeinBereich\MeinService;

class MeinJobHandler implements ScheduledJobHandlerInterface
{
    public static function definition(): array
    {
        return [
            'label'                         => 'Mein Job',
            'description'                   => 'Kurze Beschreibung was der Job macht',
            'default_enabled'               => 1,
            'default_timezone'              => defined('APP_TIMEZONE') ? (string) APP_TIMEZONE : 'UTC',
            'default_schedule_type'         => 'daily',
            'default_schedule_interval'     => 1,
            'default_schedule_time'         => '03:00',
            'default_schedule_weekdays_csv' => null,
            'default_catchup_once'          => 1,
            'allowed_schedule_types'        => ['daily', 'weekly'],
        ];
    }

    public static function execute(?int $actorUserId): array
    {
        $result = MeinService::run($actorUserId);

        if (!($result['ok'] ?? false)) {
            $errorCode = (string) ($result['error'] ?? 'job_failed');
            $status = $errorCode === 'lock_not_acquired' ? 'skipped' : 'failed';
            return [
                'status'        => $status,
                'error_code'    => $errorCode,
                'error_message' => null,
                'result'        => [],
            ];
        }

        return [
            'status'        => 'success',
            'error_code'    => null,
            'error_message' => null,
            'result'        => [
                'processed_count' => (int) ($result['processed_count'] ?? 0),
                'duration_ms'     => (int) ($result['duration_ms'] ?? 0),
            ],
        ];
    }
}

2. Job in der Registry eintragen

Datei: lib/Service/Scheduler/ScheduledJobRegistry.php

// Konstante hinzufügen:
public const MEIN_JOB = 'mein_job';

// In handlers() eine Zeile hinzufügen:
private static function handlers(): array
{
    return [
        self::USER_LIFECYCLE_RUN => UserLifecycleJobHandler::class,
        self::MEIN_JOB           => MeinJobHandler::class,  // ← neu
    ];
}

3. Fertig.

Keine anderen Dateien müssen angefasst werden. ensureSystemJobs() legt den Job beim nächsten Scheduler-Aufruf automatisch in der Datenbank an.

Regeln für Handler

definition()

  • Kein job_key-Feld der Array-Key in der Registry-Map ist der Job-Key.
  • default_timezone: Am besten APP_TIMEZONE-Konstante verwenden, Fallback 'UTC'.
  • allowed_schedule_types: Nur Typen erlauben, die der Job sinnvoll unterstützt. Ein Job der stundlich keinen Sinn ergibt, sollte hourly nicht listen.

execute()

  • Muss immer ein Array mit status, error_code, error_message, result zurückgeben.
  • status darf nur 'success', 'failed' oder 'skipped' sein.
  • error_message darf maximal 255 Zeichen lang sein (VARCHAR-Limit in DB).
  • result wird als JSON im Run-Log gespeichert nur JSON-serialisierbare Werte.
  • Wenn der zugrundeliegende Service einen eigenen Lock hat und lock_not_acquired meldet, sollte der Handler status = 'skipped' zurückgeben (kein echter Fehler).

Namenskonvention

  • Dateiname: MeinJobHandler.php (PascalCase + Handler-Suffix)
  • Konstante in Registry: SCREAMING_SNAKE_CASE (z.B. AUDIT_PURGE_RUN)
  • Job-Key in DB: snake_case (z.B. audit_purge_run)

Referenz-Implementierung

lib/Service/Scheduler/Handler/UserLifecycleJobHandler.php ist die vollständige Referenz-Implementierung mit internem Service-Lock, Fehlerbehandlung und job-spezifischem result-Payload.


Registrierte Jobs

job_key Handler-Klasse Default-Schedule
user_lifecycle_run UserLifecycleJobHandler täglich 02:15

Troubleshooting

  • lock_not_acquired:
    • Ein anderer Runner läuft bereits.
  • Job bleibt auf running:
    • stale-running Schutz erlaubt nach 2 Stunden wieder neue Runs (Schwellwert in ScheduledJobRepository::markRunning).
  • Kein fälliger Job:
    • next_run_at, enabled, timezone und schedule Konfiguration prüfen.