API трекера (tracker-web)

REST API на PHP 8.0+ / MySQL. Все ответы — JSON (UTF-8, кириллица не экранируется). Один и тот же API используют веб-страница, Claude, скрипт-диспетчер и Telegram-бот (бот обращается к тем же функциям напрямую).

Содержание

Общее

Авторизация

В системе два участника: user (человек) и claude. Кто выполняет запрос, определяется ключом или сессией, а не телом запроса.

СпособКтоКак
API-ключuser или claudeзаголовок Authorization: Bearer <ключ> или X-Api-Key: <ключ> (второй — запасной, если хостинг «съедает» Authorization)
Сессиятолько userPOST api/login с паролем → cookie trk_sid (HttpOnly, SameSite=Strict)

Ключи и хэш пароля лежат в config.php (в git не попадает, из браузера недоступен). Сгенерировать: php tools/make_secrets.php "пароль".

Объекты

Эпик (epic)

ПолеТипОписание
idintномер
titlestringназвание
descriptionstringописание (первая строка обычно «Исходники: …»)
rulesstringправила проекта (копируются из «Правил по умолчанию», потом правятся)
workdirstringпапка проекта
tagsstringключевые слова для поиска
statusstringtodo, on work, test, deploy, test_outside, ready, done
archived0/1в архиве
created_at, updated_atstringдаты

Задача (task): id, epic_id, title, description, status, position, created_at, updated_at. Баг (bug): как задача, но вместо position — task_id (к какой задаче относится, может быть null).

Комментарий (comment): id, entity_type (epic/task/bug), entity_id, parent_id (ответ на другой комментарий), author, kind (comment — обычный, note — важная заметка, question — вопрос), body, resolved (для вопроса: закрыт ответом), seen (для записей пользователя: прочитано Claude), created_at.

Вложение (file): id, entity_type, entity_id, name, path, author, created_at. Запись журнала (log): id, entity_type, entity_id, from_status, to_status, actor, created_at.

Маршруты: сводка

МетодПутьКтоНазначение
GET/meобакто я
POST/loginбез авторизациивход пользователя по паролю
POST/logoutuserвыход
GET/epics?archived=0|1обасписок эпиков со счётчиками
POST/epicsобасоздать эпик
GET/epics/{id}обаэпик целиком (задачи, баги, комментарии, файлы, журнал)
PATCH/epics/{id}обаизменить поля эпика
POST/epics/{id}/archive, /unarchiveоба; в архив эпика не в done — только userв архив / из архива
POST/epics/{id}/tasks, /bugsобасоздать задачу / баг
GET/tasks/{id}, /bugs/{id}обазадача / баг с комментариями и файлами
PATCH/tasks/{id}, /bugs/{id}обаизменить поля
DELETE/tasks/{id}, /bugs/{id}обаудалить (только в todo)
DELETE/epics/{id}только userудалить эпик навсегда (только todo или архивный)
POST/{epics|tasks|bugs}/{id}/statusоба (с ограничениями)сменить статус
GET, POST/{epics|tasks|bugs}/{id}/commentsобасписок / добавить комментарий
PATCH/comments/{id}обасменить kind / resolved
POST/{epics|tasks|bugs}/{id}/filesобазагрузить файл (multipart)
GETfile.php?id={id}обаскачать вложение (не /api)
GET/search?q=обапоиск (включая архив)
GET/inboxобанепрочитанные записи пользователя
POST/inbox/ackобаотметить прочитанными
GET, PUT/settings/{key}обанастройки (default_rules)
POST/notifyобаотправить сообщение пользователю в Telegram
*/chats…, /chat-messages/…см. разделчат с Claude (12 маршрутов)
*/secrets…только user + PINреквизиты доступов (7 маршрутов)

Сессия

GET /me

{
    "actor": "claude"
}

POST /login

Запрос:

{
    "password": "••••••••"
}

Ответ 200: {"ok": true, "actor": "user"} и cookie сессии. Неверный пароль — 401, {"detail": "Неверный пароль"}; много попыток — 429.

POST /logout

Ответ: {"ok": true}.

Эпики

GET /epics

Только неархивные; ?archived=1 — архивные. Порядок: недавно менявшиеся сверху.

[
    {
        "id": 3,
        "title": "Пример: сайт-визитка",
        "description": "Сайт на 3 страницы",
        "rules": "- Язык: PHP",
        "workdir": "D:/CLAUDE/projects_from_cloude/site",
        "tags": "сайт, пример",
        "status": "on work",
        "archived": 0,
        "created_at": "2026-10-04 20:03:47",
        "updated_at": "2026-10-04 20:03:47",
        "tasks_total": 2,
        "tasks_done": 0,
        "tasks_on_work": 1,
        "bugs_open": 0
    }
]

tasks_done — задачи в ready/done; tasks_on_work — задачи в on work; bugs_open — баги не в ready/done.

POST /epics

Запрос (обязательно только title; без rules берутся «Правила по умолчанию»):

{
    "title": "Пример: сайт-визитка",
    "description": "Сайт на 3 страницы",
    "workdir": "D:/CLAUDE/projects_from_cloude/site",
    "tags": "сайт, пример"
}

Ответ 201 — эпик целиком (как в GET /epics/{id}).

GET /epics/{id}

Эпик целиком. Сокращённый пример:

{
    "id": 3,
    "title": "Пример: сайт-визитка",
    "status": "on work",
    "archived": 0,
    "tasks": [
        {
            "id": 3,
            "epic_id": 3,
            "title": "Вёрстка главной",
            "description": "",
            "status": "on work",
            "position": 1,
            "created_at": "2026-10-04 20:03:47",
            "updated_at": "2026-10-04 20:03:47",
            "comments": [],
            "files": [],
            "log": [
                {
                    "id": 2,
                    "entity_type": "task",
                    "entity_id": 3,
                    "from_status": "todo",
                    "to_status": "on work",
                    "actor": "claude",
                    "created_at": "2026-10-04 20:03:47"
                }
            ],
            "next": ["test"]
        }
    ],
    "bugs": [],
    "comments": [
        {
            "id": 5,
            "entity_type": "epic",
            "entity_id": 3,
            "parent_id": null,
            "author": "claude",
            "kind": "question",
            "body": "Какой режим деплоя нужен?",
            "resolved": 1,
            "seen": 1,
            "created_at": "2026-10-04 20:03:47"
        }
    ],
    "files": [],
    "log": [],
    "next": ["test"],
    "open_questions": [],
    "notes": []
}

PATCH /epics/{id}

Любые из полей title, description, rules, workdir, tags. Ответ — эпик целиком. Архивный эпик не меняется (409).

POST /epics/{id}/archive и /unarchive

В архив эпик в done отправляют и user, и claude. Эпик в любом другом статусе (отменён, не нужен, застрял на открытых вопросах) в архив отправляет только user (claude получает 403) — иначе убрать такой эпик было бы нельзя. В этом случае в эпике остаётся служебная заметка [в архив из <статус>] по решению пользователя (она не попадает во «входящие»). Повторный archive для архивного эпика ничего не меняет. unarchive доступен обоим. Архивный эпик только для чтения. Ответ — эпик целиком.

DELETE /epics/{id}

Удаление эпика навсегда. Только user (иначе 403) и только для эпика в статусе todo (ещё не начат) или архивного (иначе 409: сначала отправьте эпик в архив). Каскадом удаляются задачи, баги, все комментарии, вопросы и заметки, вложения (и файлы на диске), журнал статусов и связи с сообщениями Telegram. Отменить нельзя. Ответ:

{
  "deleted": "epic #7",
  "tasks": 3,
  "bugs": 1
}

Задачи и баги

POST /epics/{id}/tasks

{
    "title": "Вёрстка главной",
    "description": "Адаптивная, под телефон"
}

Ответ 201 — задача. position ставится автоматически (следующий по счёту). Добавлять задачи и баги можно, только пока эпик в todo или on work (иначе 409).

POST /epics/{id}/bugs

{
    "title": "Форма не отправляется",
    "task_id": 3
}

task_id необязателен, но задача должна быть из этого же эпика (иначе 422).

GET /tasks/{id}, /bugs/{id}

Объект с полями comments, files, next.

PATCH /tasks/{id}, /bugs/{id}

Поля title, description; у задачи ещё position; у бага task_id (null — отвязать).

DELETE /tasks/{id}, /bugs/{id}

Только в статусе todo. Ответ: {"deleted": "task #7"}. Эпик удаляется отдельным маршрутом — DELETE /epics/{id} (см. выше).

Смена статуса

POST /{epics|tasks|bugs}/{id}/status

{
    "status": "test",
    "comment": "Проверено локально, автотесты зелёные"
}
{
    "detail": "Переход test → done запрещён. Из test можно: deploy, ready, on work"
}

Все правила — в разделе Правила переходов.

Комментарии

GET /{epics|tasks|bugs}/{id}/comments

Массив комментариев сущности по возрастанию id.

POST /{epics|tasks|bugs}/{id}/comments

{
    "body": "Какой режим деплоя нужен?",
    "kind": "question"
}
ПолеОписание
bodyтекст (обязательно)
kindcomment (по умолчанию), note, question
parent_idответ на комментарий; ответ на вопрос закрывает его (resolved = 1)
optionsмассив строк (до 8) — только для вопроса Claude: в Telegram придут кнопки-варианты, нажатие записывается как ответ
forcetrue — намеренно повторить вопрос, похожий на уже заданный

Ответ 201:

{
    "id": 5,
    "entity_type": "epic",
    "entity_id": 3,
    "parent_id": null,
    "author": "claude",
    "kind": "question",
    "body": "Какой режим деплоя нужен?",
    "resolved": 0,
    "seen": 1,
    "created_at": "2026-10-04 20:03:47"
}

PATCH /comments/{id}

{"kind": "note"} и/или {"resolved": true}.

Вложения

POST /{epics|tasks|bugs}/{id}/files

multipart/form-data, поле file. Автор берётся из ключа. Имя файла очищается, одноимённые не затираются (-2, -3…), лимит — max_upload_mb из config.php.

curl -s -X POST "$T/epics/3/files" -H "Authorization: Bearer $KEY" -F "file=@report.pdf"

Ответ 201:

{
    "id": 1,
    "entity_type": "epic",
    "entity_id": 3,
    "name": "report.pdf",
    "path": "epic-3/report.pdf",
    "author": "claude",
    "created_at": "2026-10-04 20:05:00"
}

GET file.php?id={id}

Скачивание (нужен вход или ключ). Картинки, PDF и текст открываются в браузере, остальное скачивается — это защита от выполнения чужих скриптов.

Поиск, входящие, настройки

GET /search?q=слово

Поиск без учёта регистра по эпикам (название, описание, теги, папка), задачам, багам и комментариям — включая архив.

[
    {
        "type": "epic",
        "id": 3,
        "title": "Пример: сайт-визитка",
        "epic_id": 3,
        "epic_title": "Пример: сайт-визитка",
        "archived": 0
    }
]

Для найденного комментария добавляются entity_type и entity_id.

GET /inbox

Непрочитанные записи пользователя (всё, что он написал в вебе или через Telegram). Claude смотрит это перед каждой задачей и после неё.

[
    {
        "id": 6,
        "entity_type": "epic",
        "entity_id": 3,
        "parent_id": 5,
        "author": "user",
        "kind": "comment",
        "body": "full",
        "resolved": 0,
        "seen": 0,
        "created_at": "2026-10-04 20:03:47",
        "entity_title": "Пример: сайт-визитка",
        "epic_id": 3,
        "epic_title": "Пример: сайт-визитка"
    }
]

POST /inbox/ack

{"ids": [6, 7]} — отметить эти; {} — отметить все. Ответ {"ok": true}.

GET, PUT /settings/{key}

Сейчас используется ключ default_rules (правила для новых эпиков). PUT принимает {"value": "…"}.

{
    "key": "default_rules",
    "value": "- Язык: PHP или Python.\n- Код простой, читаемый…"
}

Уведомление в Telegram

POST /notify

Отправить пользователю произвольное сообщение (например, из диспетчера: «запуск по эпику 12 завершился ошибкой»).

{
    "text": "Диспетчер: запуск Claude по эпику #12 завершился ошибкой",
    "buttons": [{"text": "Открыть трекер", "url": "https://example.com/tracker/"}]
}

buttons необязательны; через API разрешены только кнопки-ссылки. Ответ: {"queued": true}. Если бот выключен или Telegram недоступен, основной запрос всё равно успешен.

Приёмник Telegram

bot/webhook.php принимает обновления от Telegram. Это не часть REST API и не использует ключи: подлинность проверяется секретным заголовком X-Telegram-Bot-Api-Secret-Token (значение telegram.webhook_secret из config.php).

СитуацияОтвет
нет или неверный секрет403 forbidden
верный секрет200 ok (всегда, чтобы Telegram не слал повторно)
повторная доставка того же update_id200 ok, действие не выполняется
сообщение не из allowed_chat_ids200 ok, игнорируется молча

Регистрация: php bot/set_webhook.php set https://домен/путь/bot/webhook.php, проверка: … info, меню команд: … commands. Пока приёмник не размещён, для локальной проверки есть php bot/poll.php 120 (опрос getUpdates, одновременно с вебхуком не работает). Команды и кнопки бота описаны в FRONT.md.

Чат с Claude

Режим «Чат» веб-страницы: переписка человека с Claude Code. Сообщения хранятся в таблицах chats и chat_messages; ответы Claude пишет мост (dispatcher/chat_bridge.py) ключом роли claude.

Чат: id, title, epic_id (связь с эпиком необязательна; рабочая папка Claude берётся из workdir эпика), session_id (сессия Claude для --resume), status (idle — свободен, working — Claude работает, waiting — ждёт ответа на запрос разрешения), archived.

Сообщение: id, chat_id, role, body, meta, state, created_at, updated_at.

roleкто пишетчто это
useruserсообщение человека; state: pending (ждёт моста) → taken (мост забрал)
assistantclaudeответ Claude (может дописываться по частям — стриминг)
toolclaudeстрока вызова инструмента; meta: tool, status (ок/ошибка); в body — команда и результат
systemclaudeслужебная строка (например, ошибка запуска)
permissionclaudeкарточка запроса разрешения; meta: request_id, tool, rule, input; state: waiting → done, решение — meta.decision (allow/deny)
МетодПутьКтоНазначение
GET/chats?archived=0|1обасписок чатов с последним сообщением
POST/chatsобасоздать чат: title, epic_id (необязательно)
GET/chats/pendingclaudeчаты с новыми сообщениями пользователя (для моста)
GET/chats/{id}?since=обачат с сообщениями; since (время YYYY-MM-DD HH:MM:SS) — только изменённые с этого момента
PATCH/chats/{id}title, epic_id — оба; session_id, status — только claudeизменить поля
DELETE/chats/{id}обаудалить чат со всеми сообщениями
POST/chats/{id}/archive, /unarchiveобав архив / из архива
POST/chats/{id}/messagesобановое сообщение (user — от человека, остальные роли — от claude)
PATCH/chat-messages/{id}только claudeзаменить body, дописать append, слить meta (стриминг)
POST/chat-messages/{id}/takeclaudeзабрать сообщение пользователя (pending → taken); повторно — 409
POST/chat-messages/{id}/answerтолько userответ на запрос разрешения: {"decision": "allow"|"deny"}

Пример: человек пишет сообщение.

POST /api/chats/3/messages
{
    "body": "Посмотри, что изменилось в проекте"
}

Ответ 201:

{
    "id": 12,
    "chat_id": 3,
    "role": "user",
    "body": "Посмотри, что изменилось в проекте",
    "meta": {},
    "state": "pending",
    "created_at": "2026-10-06 14:29:14",
    "updated_at": "2026-10-06 14:29:14"
}

Пример: Claude просит разрешение (мост, роль claude); чат переходит в waiting.

POST /api/chats/3/messages
{
    "role": "permission",
    "body": "Bash: rm -rf cache",
    "meta": {
        "request_id": "tu1",
        "tool": "Bash",
        "rule": "Bash(rm -rf cache)",
        "input": {"command": "rm -rf cache"}
    }
}

Человек отвечает; когда ожидающих карточек не осталось, чат возвращается в working, и мост повторяет запуск с этим разрешением.

POST /api/chat-messages/15/answer
{
    "decision": "allow"
}

Ответ — сообщение с state: "done" и meta.decision: "allow".

Ошибки: 403 — роль не может выполнить действие (например, user пишет assistant или меняет status); 404 — нет чата/сообщения; 409 — чат в архиве, сообщение уже забрано, не permission или уже отвечено; 422 — пустое или слишком длинное сообщение, неверные role/status/decision, у permission нет meta.request_id и meta.tool.

Секреты

Раздел «Секреты» хранит реквизиты доступов к проектам (проект, аннотация, значение). Доступ только у роли user и только после ввода PIN-кода (secrets_pin в config.php). Ключ claude всегда получает 403 — Claude эти данные не читает. Значения не попадают в поиск, журнал и уведомления Telegram.

PIN вводится в браузере (POST /secrets/unlock; разблокировка живёт 15 минут в сессии) или передаётся скриптом заголовком X-Secrets-Pin. Неверный PIN: 5 попыток с одного адреса за 15 минут, затем 429.

МетодПутьКтоНазначение
GET/secrets/statususerоткрыт ли раздел, сколько секунд осталось, задан ли PIN
POST/secrets/unlockuserввести PIN: {"pin": "…"}
POST/secrets/lockuserзакрыть раздел
GET/secretsuser + PINвсе записи
POST/secretsuser + PINсоздать: project, title, value
PATCH/secrets/{id}user + PINизменить поля
DELETE/secrets/{id}user + PINудалить

Пример: запись (значение условное).

POST /api/secrets
{
    "project": "ПРОЕКТ",
    "title": "FTP-доступ на хостинг (тестовый)",
    "value": "host: example.com\nлогин: demo\nпароль: ********"
}

Ответ 201:

{
    "id": 7,
    "project": "ПРОЕКТ",
    "title": "FTP-доступ на хостинг (тестовый)",
    "value": "host: example.com\nлогин: demo\nпароль: ********",
    "created_at": "2026-10-07 23:30:00",
    "updated_at": "2026-10-07 23:30:00"
}

Ошибки: 403 — роль claude или раздел закрыт (нет PIN); 401 — неверный PIN в unlock; 429 — слишком много неверных PIN; 409 — secrets_pin не задан в конфиге; 422 — пустой project или title; 404 — нет записи.

Правила переходов

Цепочка одинакова для эпиков, задач и багов: todo → on work → test → deploy → test_outside → ready → done. Локальный проект без сервера: test → ready.

ИзМожно в
todoon work
on worktest
testdeploy, ready, on work
deploytest_outside, on work
test_outsideready, on work
readydone, on work
doneon work

Дополнительно (сервер отвечает 409 с причиной):

Коды ошибок

КодКогда
401нет ключа/сессии, неверный ключ или пароль
403actor/author не совпадает с ключом; нет заголовка X-Requested-With при работе по сессии; неверный секрет webhook
404нет такого маршрута, эпика, задачи, настройки
409запрещённый переход статуса, архивный эпик, повторный вопрос, закрытые двери по правилам выше
422неверные данные (пустое название, неверный тип поля, битый JSON, файл не получен или больше лимита)
429слишком много неудачных входов
500внутренняя ошибка (подробности — в логе PHP на сервере)