""" DAG: sync jadwal sidang eDOXID -> Google Calendar "Jadwal Sidang" + reminder email. DAG ini trigger tiap 30 menit sepanjang hari (1 DAG, 1 schedule — bukan 2 DAG terpisah untuk jam-kerja/luar-jam-kerja, biar sederhana), tapi `run_sync()` sendiri yang memutuskan kerja BENERAN atau skip: - Jam kerja (default 07:00-17:00 WIB, EDOXID_BUSINESS_HOURS_START/END): tiap trigger (~30 menit) langsung kerja penuh. - Di luar jam kerja: cuma kerja penuh kalau sudah >= EDOXID_OFF_HOURS_INTERVAL_MINUTES (default 120 menit) sejak sync terakhir; kalau belum, skip (tidak buka koneksi DB/Calendar/Gmail sama sekali, murah). Alasan: 30 menit sepanjang hari dianggap kurang efisien (bukan soal Google menganggap ini "bot" — ini OAuth API resmi dengan izin, bukan scraping — tapi soal efisiensi jumlah run), tapi tetap ingin responsif di jam kerja. Kerja penuh artinya: 1. create/update/delete event Google Calendar sesuai jadwal sidang yang baru/berubah/dibatalkan. 2. Kirim email reminder terpisah (subjek "[PENGINGAT SEBELUM H-1]" dan "[PENGINGAT 1 JAM SEBELUM]") ke semua attendee (mahasiswa+pembimbing+penguji) pas jam H-1 07:00 dan 1 jam sebelum sidang — karena reminders.overrides bawaan Calendar API cuma berlaku untuk kalender pemilik event (TU), TIDAK ikut ke kalender pribadi tiap attendee (keterbatasan Google Calendar, dikonfirmasi 2026-07-27). Auth Google pakai OAuth refresh token sebagai tu@informatika.untan.ac.id langsung (bukan service account — service account tidak bisa invite attendee tanpa Domain-Wide Delegation, sudah dicoba dan ditolak Google 2026-07-27). Refresh token perlu scope gabungan: calendar + gmail.send. Tidak menyentuh kode/DB aplikasi eDOXID (`/var/www/manajemen-usulan`) sama sekali selain query SELECT read-only ke 3 tabel: submissions, verificators, users. Environment yang wajib di-set di Coolify (service airflow-scheduler): EDOXID_DB_PASSWORD EDOXID_GOOGLE_CLIENT_ID, EDOXID_GOOGLE_CLIENT_SECRET, EDOXID_GOOGLE_REFRESH_TOKEN (refresh token WAJIB scope "calendar" + "gmail.send" sekaligus, bukan cuma calendar) Opsional (lihat scripts/edoxid_calendar_config.py untuk default): EDOXID_DB_HOST, EDOXID_DB_PORT, EDOXID_DB_DATABASE, EDOXID_DB_USERNAME, EDOXID_GOOGLE_CALENDAR_ID, EDOXID_STATE_SQLITE_PATH, EDOXID_SYNC_MIN_DATE, EDOXID_BUSINESS_HOURS_START (default 7), EDOXID_BUSINESS_HOURS_END (default 17), EDOXID_OFF_HOURS_INTERVAL_MINUTES (default 120) """ from datetime import datetime, timedelta from airflow import DAG from airflow.operators.python import PythonOperator from scripts.edoxid_calendar_sync import run_sync with DAG( dag_id="edoxid_calendar_sync", description="Sync jadwal sidang eDOXID ke Google Calendar 'Jadwal Sidang'", start_date=datetime(2026, 7, 27), schedule_interval="*/30 * * * *", catchup=False, max_active_runs=1, default_args={ "owner": "informatika", "retries": 1, "retry_delay": timedelta(minutes=1), }, tags=["edoxid", "calendar", "sidang"], ) as dag: sync_task = PythonOperator( task_id="sync_jadwal_sidang", python_callable=run_sync, )