Конфигурация проекта
Frames (встроенные iframe) и OpenAPI-спеки, добавленные через UI панели, сохраняются в JSON-файл рядом с исходниками вашего приложения. Закоммитьте этот файл — и каждый разработчик команды после git pull получает идентичный сетап. Никакого «у меня работает».
Внутри панель держит локальный кэш в localStorage для оффлайн-UX, но источник истины — файл на бэкенде: на каждой загрузке страницы панель забирает свежую версию, а ваши правки дебаунсятся (500 мс) в один PUT.
Структура файлов
Панель пишет два файла в директорию, специфичную для каждого фреймворка:
<your-app>/
└── config/
└── adp/
├── project.json ← коммитьте — общий с командой
└── .gitignore ← создаётся автоматически, игнорирует secrets.json| Файл | Коммитить? | Содержимое |
|---|---|---|
project.json | Да | {version, frames, openapi} — карты «отображаемое имя → URL» |
.gitignore | Да | Автогенерируется с secrets.json |
secrets.json | Нет (gitignored) | API-ключи, OAuth-токены, ACP-окружение. Только локально, права 0600 |
Пример project.json:
{
"version": 1,
"frames": {
"Grafana": "https://grafana.example.com/",
"Logs": "https://kibana.example.com/"
},
"openapi": {
"Main API": "/api/openapi.json",
"Webhooks": "https://webhooks.example.com/openapi.json"
}
}Структура нарочно простая: каждая запись — пара «имя → URL». Файл можно править вручную, главное оставаться валидным JSON.
Путь конфигурации по фреймворкам
Каждый адаптер кладёт директорию в место, идиоматичное для своего фреймворка, и предоставляет рычаг переопределения.
По умолчанию: <корень-проекта>/config/adp — резолвится через alias @root из Yiisoft Aliases.
Переопределение в config/params.php:
'app-dev-panel/yii3' => [
// ...
'projectConfigPath' => '@root/config/adp', // по умолчанию
// 'projectConfigPath' => '@root/.adp', // например, скрыть в точечной директории
],Принимает любой alias, который понимает Yiisoft Aliases (@root, @runtime и т.д.), либо абсолютный путь.
API-эндпоинт
Фронтенд общается с бэкендом через два эндпоинта под /debug/api/project:
| Метод | Путь | Назначение |
|---|---|---|
GET | /debug/api/project/config | Возвращает {config: {version, frames, openapi}, configDir}. configDir — абсолютный путь, который нужно git add |
PUT | /debug/api/project/config | Принимает «голый» документ {frames, openapi} или обёртку как у GET. Битые записи (нестроковые ключи/значения, пустые строки) тихо отбрасываются |
Можно проверить установку через curl:
curl http://127.0.0.1:8101/debug/api/project/config | jq
# {
# "data": {
# "config": {"version": 1, "frames": {}, "openapi": {}},
# "configDir": "/home/you/app/config/adp"
# }
# }curl -X PUT \
-H 'Content-Type: application/json' \
-d '{"frames":{"Grafana":"https://grafana.example/"},"openapi":{}}' \
http://127.0.0.1:8101/debug/api/project/configПосле первого PUT директория config/adp/ и оба файла появляются на диске.
Как фронтенд синхронизирует данные
Панель работает по dual-store схеме, чтобы оставаться рабочей при недоступном бэкенде:
- При загрузке панель отправляет
getProjectConfig. Документ с сервера перезаписывает локальные slices Frames/OpenAPI в Redux. - При правке (добавление/удаление/переименование Frame или OpenAPI-спеки) изменение сначала применяется локально (мгновенный UI), затем дебаунсится на 500 мс в один
PUT. - Миграция при первом запуске: если бэкенд вернул пустой конфиг, а в
localStorageуже есть записи (типичная ситуация при апгрейде с более ранней версии ADP), панель один раз выгружает их на сервер — никто не теряет свой сетап. - Бэкенд недоступен: диалог настроек показывает явное предупреждение. Правки остаются в
localStorage; на следующей удачной загрузке они синхронизируются.
Диалог настроек также показывает путь к configDir, чтобы вы знали какой файл коммитить:
┌────────────────────────────────────────────┐
│ Frames │
│ … │
│ │
│ ⓘ Synced to /your-app/config/adp/ │
│ project.json. Commit it to share with │
│ your team. │
└────────────────────────────────────────────┘Проверка по плейграундам
Каждый плейграунд пишет конфиг в место, естественное для своего фреймворка. Запустите серверы (make serve) и опросите их curl-ом:
for p in 8101 8102 8103 8104; do
curl -s "http://127.0.0.1:$p/debug/api/project/config" | jq -r '.data.configDir'
done| Плейграунд | Порт | configDir |
|---|---|---|
| Yii 3 | 8101 | playground/yii3-app/config/adp |
| Symfony | 8102 | playground/symfony-app/config/adp |
| Yii 2 | 8103 | playground/yii2-basic-app/src/config/adp |
| Laravel | 8104 | playground/laravel-app/config/adp |
| Spiral | 8105 | playground/spiral-app/app/config/adp |
(У Yii 2 путь оказывается под src/, потому что в плейграунде @app указывает на эту директорию — в реальном приложении alias резолвится по-другому.)
Файл секретов (secrets.json)
Локальный сосед project.json для значений, которые ни в коем случае не должны попасть в VCS: API-ключи, OAuth-токены, ACP-окружение. Правило в .gitignore создаётся автоматически при первой записи project.json, поэтому свежий чекаут безопасен по умолчанию.
Структура (v1):
{
"version": 1,
"llm": {
"apiKey": "sk-ant-...",
"provider": "openrouter",
"model": "anthropic/claude-opus-4-7",
"timeout": 30,
"customPrompt": "...",
"acpCommand": "claude",
"acpArgs": [],
"acpEnv": {}
}
}llm-namespace в точности повторяет историчный runtime/.llm-settings.json — это позволяет в будущем дописывать другие категории секретов (DB credentials, OAuth-токены других провайдеров, …) без миграции схемы. Права файла — 0600 (чтение/запись только владельцу).
Миграция со старого runtime/.llm-settings.json
При первом чтении LLM-настроек после апгрейда панель автоматически мигрирует старый файл: содержимое уезжает в secrets.json, оригинал переименовывается в .llm-settings.json.migrated (остаётся как бэкап, не удаляется), в stderr выводится одна строка-уведомление. Идемпотентно — второй запуск ничего не делает.
API-эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
GET | /debug/api/project/secrets | Маскированный снимок. apiKey показывает только последние 4 символа ("...wxyz"); значения acpEnv и элементы acpArgs тоже маскируются. Булевы hasApiKey / hasAcpArgs позволяют UI рендерить «настроено / не настроено», ни разу не загрузив реальный секрет в SPA. |
PATCH | /debug/api/project/secrets | Merge-обновление. Тело: {llm: {<поле>: <значение-или-null>}}. null удаляет ключ, отсутствующие ключи остаются нетронутыми. PUT нет — маскированный GET-ответ намеренно нероундтриппабелен. |
Быстрая проверка из терминала:
# Сохранить ключ.
curl -X PATCH \
-H 'Content-Type: application/json' \
-d '{"llm":{"apiKey":"sk-ant-XXXXXXXX","provider":"anthropic"}}' \
http://127.0.0.1:8101/debug/api/project/secrets
# Прочитать обратно — маскированный.
curl http://127.0.0.1:8101/debug/api/project/secrets | jq '.data.secrets'
# {
# "apiKey": "...XXXX",
# "hasApiKey": true,
# "provider": "anthropic",
# ...
# }
# Удалить ключ явно.
curl -X PATCH \
-H 'Content-Type: application/json' \
-d '{"llm":{"apiKey":null}}' \
http://127.0.0.1:8101/debug/api/project/secretsСуществующие LLM-эндпоинты (/debug/api/llm/connect, /debug/api/llm/oauth/exchange, /debug/api/llm/disconnect, /debug/api/llm/model, …) не изменились — внутри они теперь пишут через secrets.json, а не runtime/.llm-settings.json.
Real-time синхронизация (SSE)
Панель слушает GET /debug/api/project/event-stream для push-уведомлений при изменении project.json или secrets.json. Эндпоинт делает stat() обоих файлов раз в секунду и эмитит:
data: {"type":"project-config-stream-ready"} # отправляется при подключении
data: {"type":"project-config-changed"} # отправляется на каждое изменениеЭто ловит три разных источника:
- Сама панель сохранила что-то через
PUT/PATCH. - Другой таб браузера редактирует тот же бэкенд.
git pullпереписал файл извне процесса.
projectSyncMiddleware на фронте реагирует force-refetch'ем getProjectConfig и getSecrets. Существующий обработчик getProjectConfig.fulfilled ре-гидрирует OpenAPI/Frames slices — тот же путь, что и при начальном бутстрапе.
Соединение автоматически закрывается через 30 секунд; браузерный EventSource сам переподключается. Это не даёт PHP-built-in-server-воркеру висеть вечно и переживает кратковременные сбои сети.
Однопоточные dev-серверы
По умолчанию php -S — однопоточный, и одно открытое SSE-соединение блокирует все остальные запросы на том же порту. Плейграунды решают это через PHP_CLI_SERVER_WORKERS=3, который ставит bin/serve.sh. В своём dev-окружении используйте многопроцессный SAPI (PHP-FPM, FrankenPHP, RoadRunner) или выставьте PHP_CLI_SERVER_WORKERS перед php -S.
Технические детали
- Project storage: FileProjectConfigStorage
AppDevPanel\Kernel\Project\FileProjectConfigStorage— атомарная запись (временный файл + rename), права0644, авто-создание директории и.gitignore. - Project interface: ProjectConfigStorageInterface
AppDevPanel\Kernel\Project\ProjectConfigStorageInterface. - Project VO: ProjectConfig
AppDevPanel\Kernel\Project\ProjectConfig— иммутабельный, отбрасывает битые записи вfromArray(). - Secrets storage: FileSecretsStorage
AppDevPanel\Kernel\Project\FileSecretsStorage— атомарная запись с правами0600. - Secrets interface: SecretsStorageInterface
AppDevPanel\Kernel\Project\SecretsStorageInterface. - Secrets VO: SecretsConfig
AppDevPanel\Kernel\Project\SecretsConfig— иммутабельный, поддерживаетwithLlm()(заменить) иwithLlmPatch()(merge сnull= удалить). - HTTP-контроллеры: ProjectController
AppDevPanel\Api\Project\Controller\ProjectController(/config+/event-stream), SecretsControllerAppDevPanel\Api\Project\Controller\SecretsController(/secrets). - LLM-фасад: FileLlmSettings
AppDevPanel\Api\Llm\FileLlmSettingsClass FileLlmSettings. — читает/пишет черезSecretsStorage, автоматически мигрирует legacyruntime/.llm-settings.jsonпри первом чтении. - Frontend-модуль:
libs/frontend/packages/panel/src/Module/Project/. - Sync-middleware:
Module/Project/projectSyncMiddleware.ts— bootstrap, debounce, миграция, SSE-переподключение, подавление feedback-loops. - RTK Query API:
libs/frontend/packages/sdk/src/API/Project/Project.ts(getProjectConfig,updateProjectConfig,getSecrets,patchSecrets).