@startuml Wecker_Cronjob_System

!theme plain
skinparam defaultFontName "Arial"
skinparam defaultFontSize 11
skinparam backgroundColor #FAFAFA
skinparam ArrowColor #2c3e50
skinparam ActivityBorderColor #2c3e50
skinparam ActivityBackgroundColor #EBF5FB
skinparam ActivityDiamondBackgroundColor #FEF9E7
skinparam ActivityDiamondBorderColor #D4AC0D
skinparam NoteBackgroundColor #EAFAF1
skinparam NoteBorderColor #27AE60
skinparam ActivityBarColor #2c3e50
skinparam PartitionBackgroundColor #F4F6F7
skinparam PartitionBorderColor #85929E

title <b>Wecker – Cronjob-System</b>\n<size:11>Vollständiger Ablauf des Master-Cronjobs und aller Sub-Jobs</size>

|#D6EAF8|**System**|
start

:🖥️ <b>Raspberry Pi Boot</b>;

partition "wecker_cron_systemd.sh" {
  :<b>systemd Unit-Datei erzeugen</b>\n/etc/systemd/system/wecker-cron.service\nType=simple, Restart=always, RestartSec=5;
  note right
    ExecStart: php /var/www/wecker/cronjob.php
    Startet automatisch nach:
    - network.target
    - mysql.service / mariadb.service
  end note
  :systemctl enable wecker-cron\nsystemctl start wecker-cron;
}

|#D5F5E3|**cronjob.php (Master-Prozess)**|

:<b>config.php</b> laden\nDB-Verbindung (PDO) herstellen\nFunktionen laden;

if (config.php vorhanden? && DB-Verbindung OK?) then (Nein)
  :Fehler → STDERR\nExit(1);
  stop
else (Ja)
endif

:<b>PID schreiben</b>\nstorage/cronjob_master.pid\nstorage/cronjob_master.json\n{ pid, started_at, last_activity };
note right
  Singleton-Schutz:
  Nur ein Master gleichzeitig aktiv.
  Älterer Prozess wird beendet.
end note

repeat

  :masterCheckCounter++;

  if (masterCheckCounter >= 30?) then (Ja)
    :masterCheckCounter = 0;
    if (PID-Datei vorhanden?) then (Nein)
      :Eigene PID neu schreiben;
    else (Ja)
      if (Andere PID läuft noch\n(posix_kill / tasklist)?) then (Ja – abgelöst)
        :Prozess beendet sich\nExit(0);
        stop
      else (Nein – übernehmen)
        :Eigene PID in Datei überschreiben;
      endif
    endif
  else (Nein)
  endif

  :<b>Aktive Jobs aus DB laden</b>\nSELECT id, name, interval_sekunden,\nletzte_ausfuehrung\nFROM cronjobs\nWHERE aktiv = 1\nAND interval_sekunden > 0;

  fork
    :<b>Für jeden Job prüfen:</b>\nnow − letzte_ausfuehrung ≥ interval_sekunden?;

    if (Job fällig?) then (Ja)
      :<b>include cronjob/{name}.php</b>;
      :UPDATE cronjobs\nSET letzte_ausfuehrung = NOW()\nWHERE id = ?;
    else (Nein – überspringen)
    endif
  fork again
    note right
      <b>Alle Jobs laufen parallel geprüft</b>\n(sequentielle include-Aufrufe)
    end note
  end fork

  :<b>cronjob_master.json</b> aktualisieren\nlast_activity = NOW();

  :sleep(1 Sekunde);

repeat while (<b>Endlosschleife</b> – läuft dauerhaft)

|#FEF9E7|**Sub-Job: timer_pruefung**\n(interval = 1 Sek.)|

:<b>alarm_ringing.json</b> lesen\n(aktueller Klingelzustand);
note right
  storage/alarm_ringing.json
  { ringing, alarm_id, alarm_time,
    cover_url, audio_urls, started_at }
end note

if (ringing=true &&\ngestartet vor >120 Sek?) then (Ja – Auto-Expire)
  :ringing = false\nalarm_ringing.json schreiben;
else (Nein)
endif

if (Wecker klingelt gerade\n(ringing = true)?) then (Ja)
  :Nichts tun – warten auf\nBenutzer (Snooze/Stop);
  :→ return;
else (Nein)
endif

:<b>Alle aktiven Wecker holen</b>\nfunction_wecker_hole_aktive();

:Für jeden Wecker prüfen:;

switch (Wiederholungstyp?)
  case (einmal)
    :weckdatum == heute\n&& weckzeit == HH:MM;
  case (taeglich)
    :weckzeit == HH:MM;
  case (woechentlich)
    :Wochentag in Liste\n&& weckzeit == HH:MM;
endswitch

if (Soll klingeln?) then (Ja)
  if (Snooze aktiv &\nnoch nicht abgelaufen?) then (Ja)
    :überspringen;
  else (Nein)
    if (In dieser Minute\nbereits gestoppt?) then (Ja)
      :überspringen;
    else (Nein)
      :<b>Audio-URLs auflösen</b>\nfunction_wecker_audio_urls()\n→ gleichzeitig / danach;
      :TTS-Cache prüfen\nSELECT aus wecker_tts_cache\nWHERE text_hash = sha256(text);

      if (TTS gecacht?) then (Ja)
        :ttsUrl = /tts_file.php?f=...;
      else (Nein)
        :kein TTS-URL;
      endif

      :<b>alarm_ringing.json schreiben</b>\n{ ringing: true, alarm_id,\nalarm_time, cover_url,\naudio_urls, started_at };
      :UPDATE wecker SET\nzuletzt_ausgeloest = NOW()\nWHERE id = ?;
      note right
        Browser pollt /api_wecker.php?action=state
        und startet Audio-Player wenn ringing=true
      end note
    endif
  endif
else (Nein)
endif

|#FDEDEC|**Sub-Job: tts_vorwecker**\n(interval aus DB, z.B. 60 Sek.)|

:<b>Alle aktiven Wecker holen</b>\nfunction_wecker_hole_aktive();
:Alle Wochentage laden\nfunction_wecker_hole_wochentage_alle();
:Zeitzone aus Einstellungen holen;

:Für jeden Wecker mit Sprachtext:;

:<b>Nächste Klingelzeit berechnen</b>\nfunction_wecker_naechste_klingelzeit();

if (Nächste Klingelzeit\nin den nächsten 10 Min?) then (Ja)
  :<b>Platzhalter ersetzen</b>\nfunction_tts_text_ersetzen()\n"Guten Morgen, es ist 7:00 Uhr...";
  if (Text bereits im\nTTS-Cache vorhanden?) then (Ja)
    :Überspringen – kein neues Audio;
  else (Nein)
    :<b>TTS generieren</b>\nfunction_tts_generieren();
    :Audio-Datei in\nstorage/tts/ speichern;
    :<b>In Cache eintragen</b>\nfunction_tts_cache_set()\nINSERT INTO wecker_tts_cache;
  endif
else (Nein – zu weit weg)
endif

|#F3E5F5|**Sub-Job: refresh_wetter**\n(interval = 1800 Sek. / 30 Min.)|

:<b>Wetter-Ort aus Einstellungen</b>\nfunction_einstellung_hole('wetter_ort');

if (Ort konfiguriert?) then (Ja)
  :<b>Wetter-API aufrufen</b>\nfunction_wetter_fetch(ort)\n→ Open-Meteo API;
  if (Erfolgreich?) then (Ja)
    :Wetterdaten in Cache schreiben;
  else (Nein)
    :error_log Fehler;
  endif
else (Nein)
  :return – nichts tun;
endif

|#E8F8F5|**Sub-Job: collect_stats**\n(interval = 60 Sek.)|

:CPU-Temperatur lesen;

fork
  :Methode 1: sysfs\n/sys/class/thermal/\nthermal_zone0/temp;
fork again
  :Methode 2: vcgencmd\nmeasure_temp;
end fork

:Stromverbrauch lesen;

fork
  :Pi 5: sysfs\n/sys/class/power_supply/CHARGER/;
fork again
  :Pi 4: vcgencmd\npmic_read_adc;
fork again
  :Generisch: sysfs\npower_supply/*/;
end fork

:volt × amp = watts berechnen;

:<b>In DB speichern</b>\nINSERT INTO statistiken\n(zeitstempel, cpu_temp, watts, volt, amp);
:<b>Alte Daten bereinigen</b>\nDELETE WHERE zeitstempel <\nDATE_SUB(NOW(), INTERVAL 7 DAY);

|#FFF9C4|**Manueller Trigger**|

:<b>api_info.php?action=run</b>\n(Einstellungen → Info-Seite);
:<b>cronjob_run_once.php</b>\nAlle aktiven Jobs einmalig ausführen\n(kein Intervall-Check);
:State-Datei aktualisieren\nlast_activity = NOW();

stop

@enduml
