--- created: 2026-06-25 tags: [eagle-dashboard, launchd, cron, macos, devops] updated: 2026-06-25 --- # Eagle Dashboard — Launchd Integration ## Обзор Eagle Dashboard ([eagle-dash](https://github.com/mallexxx/eagle-dash)) имеет полноценную поддержку launchd-джобов наравне с Hermes Eagle/Whale кронами. Пользователь может создавать, просматривать, запускать/останавливать и удалять launchd-агенты прямо из интерфейса дашборда. ## Архитектура ### Backend (`main.py`) Три ключевые функции: - **`_load_launchd_plist(plist)`** — читает один `.plist` файл, парсит его, вызывает `launchctl list` для статуса (PID, LastExitStatus, enabled), возвращает унифицированный словарь. - **`_list_all_launchd()`** — читает все `*.plist` из `~/Library/LaunchAgents/`, вызывает `_load_launchd_plist()` для каждого. - **`_list_launchd_crons()`** — фильтрует только те, у которых есть `StartInterval` или `StartCalendarInterval`. - **`_list_launchd_services()`** — фильтрует те, у которых НЕТ расписания (always-on демоны). ### API endpoints | Endpoint | Метод | Описание | |----------|-------|----------| | `/api/crons?source=launchd` | GET | Список launchd-кронов (с расписанием) | | `/api/services/launchd` | GET | Список launchd-сервисов (без расписания, always-on) | | `/api/crons` | POST | Создание нового launchd-агента (source=launchd) | | `/api/crons/launchd/{label}` | PATCH | Обновление launchd-агента (plist) | | `/api/crons/launchd/{label}` | DELETE | Удаление launchd-агента | | `/api/crons/launchd/{label}/toggle` | POST | Вкл/выкл launchd-агента (launchctl load/unload) | ### Создание launchd-крона (POST /api/crons) - Поле `source: "launchd"` в body - `name` — Label для plist (если не начинается с `com.`, добавляется префикс `com.`) - `schedule` — парсится как `every Ns/m/h/d` → `StartInterval` в секундах - `script` — `ProgramArguments` (разбивается по пробелам) - `workdir` — `WorkingDirectory` - `run_at_load` (boolean) — `RunAtLoad` в plist - Поля `prompt`, `deliver`, `skills`, `toolsets`, `model`, `provider` — **скрыты** на фронте, не отправляются для launchd - Plist пишется атомарно через `.plist.tmp` → `.replace()` - После записи — `launchctl load -w` ### Чтение статуса launchd `launchctl list {label}` на macOS (Sequoia 15.x, next-формат) возвращает openstep-style plist (не JSON, не tab-separated): ``` { "PID" = 93796; "LastExitStatus" = 0; "Label" = "com.eagle.dashboard"; ... } ``` Парсинг через regex: `r'"PID"\s*=\s*(\d+)'` и `r'"LastExitStatus"\s*=\s*(-?\d+)'`. **Логика статуса:** - `state = "running"` — если PID > 0 - `state = "scheduled"` — если есть расписание, процесс не запущен - `state = "stopped"` — если disabled (launchctl вернул non-zero) - `state = "idle"` — enabled, без расписания, без PID - `last_status = None` если процесс запущен (PID есть), иначе числовой exit code - `last_status = null` — ни разу не запускался ### Фронтенд (`templates/index.html`) - **Services вкладка** — показывает Docker контейнеры, сервисы из registry, **и** секцию "Launchd (Always-On)" с always-on демонами. Карточки launchd-сервисов имеют кнопки Pause/Resume и Delete. - **Crons вкладка** — показывает Hermes Eagle/Whale кроны + launchd-кроны (только с расписанием). Для launchd-кронов показывается schedule, exit code, runs count (runs не реализован из-за отсутствия данных в launchd). - **Модалка создания** — при выборе `source = launchd` скрываются поля: Deliver, Prompt, Model+Provider, Skills+Toolsets; показывается чекбокс "Run on load" (`runAtLoad`, отправляется как `run_at_load: boolean`) - **Schedule hint** — для launchd показывается упрощённый хинт: только `every Ns/m/h/d` - **Tab URL persistence** — через `history.replaceState` + URLSearchParams (`?tab=crons`) - **Контролы на launchd-сервисах (Services вкладка)**: кнопка ⏸ pause / ▶ resume (вызывает `/api/crons/launchd/{label}/toggle`), кнопка 🗑 delete (вызывает `DELETE /api/crons/launchd/{label}`) ## Ключевые решения 1. **Зачем отдельный launchd CRUD** — чтобы пользователь мог управлять своими кронами из единого интерфейса, не открывая терминал. 2. **Разделение crons/services** — launchd-агенты без `StartInterval`/`StartCalendarInterval` не показываются в Crons. Вместо этого они отображаются на Services вкладке как "Launchd (Always-On)". 3. **PID vs LastExitStatus** — для запущенного процесса exit code неактуален, поэтому `last_status = None` при PID > 0. 4. **Next-формат** — macOS Sequoia возвращает openstep plist (не tab-separated, не JSON): `"Key" = value;`. Парсинг через regex. 5. **Атомарная запись plist** — через `.tmp` + `.replace()` чтобы не повредить plist при ошибке записи. 6. **`_list_launchd_crons()` vs `_list_launchd_services()`** — обе читают все plist из `_list_all_launchd()`, но фильтруют по `has_schedule`. Используют общий `_load_launchd_plist()`. ## Файлы - `/Users/admin/Developer/eagle-dash/main.py` — backend - `/Users/admin/Developer/eagle-dash/templates/index.html` — frontend - `/Users/admin/Developer/eagle-dash/.venv/` — virtualenv (FastAPI + uvicorn) - Запуск через launchd: `~/Library/LaunchAgents/com.eagle.dashboard.plist` ## Статус (2026-06-25) - [x] Backend CRUD для launchd (create, read, update, delete, toggle) - [x] Список launchd-кронов на Crons вкладке - [x] Список always-on launchd-демонов на Services вкладке - [x] Чекбокс RunAtLoad при создании - [x] Правильные статусы: running / scheduled / idle / stopped + exit code - [x] PID и LastExitStatus читаются из launchctl list (next-format) - [x] Спрятаны поля для launchd (prompt, deliver, skills и т.д.) - [x] Tab URL persistence - [x] Кнопки Pause/Resume/Delete на карточках launchd (и в Crons, и в Services) - [x] Разделение crons/services: с расписанием → Crons, без расписания → Services - [x] `_list_launchd_services()` — новый endpoint `/api/services/launchd`