API трекера (tracker-web)
REST API на PHP 8.0+ / MySQL. Все ответы — JSON (UTF-8, кириллица не экранируется). Один и тот же API используют веб-страница, Claude, скрипт-диспетчер и Telegram-бот (бот обращается к тем же функциям напрямую).
Содержание
Общее
- Базовый адрес:
<папка проекта>/api/…, напримерhttps://example.com/tracker/api/epics. Пути в проекте относительные, поэтому папку можно положить куда угодно. Если на хостинге нетmod_rewrite, работает иapi.php/epics. - Тело запросов — JSON (
Content-Type: application/json; charset=utf-8), кроме загрузки файла (multipart/form-data). - Даты — строки
ГГГГ-ММ-ДД ЧЧ:ММ:ССв часовом поясе изconfig.php. - Булевы поля (
archived,resolved,seen) — числа0/1. - Ошибка — всегда
{"detail": "понятное объяснение"}и подходящий HTTP-код. - Кириллица в curl под Windows: передавайте JSON через stdin (
--data-binary @-), а не в аргументах — иначе тело портится.
Авторизация
В системе два участника: user (человек) и claude. Кто выполняет запрос, определяется ключом или сессией, а не телом запроса.
| Способ | Кто | Как |
|---|---|---|
| API-ключ | user или claude | заголовок Authorization: Bearer <ключ> или X-Api-Key: <ключ> (второй — запасной, если хостинг «съедает» Authorization) |
| Сессия | только user | POST api/login с паролем → cookie trk_sid (HttpOnly, SameSite=Strict) |
Ключи и хэш пароля лежат в config.php (в git не попадает, из браузера недоступен). Сгенерировать: php tools/make_secrets.php "пароль".
- Если в теле указано
actor/author, оно должно совпадать с владельцем ключа, иначе403(нельзя выдать себя за другого). Если не указано — подставляется владелец ключа. - Запросы по сессии, меняющие данные (не GET), обязаны содержать заголовок
X-Requested-With: tracker— защита от CSRF. Запросы по ключу этого не требуют. - Срок сессии — 30 дней с последнего запроса. Каждый запрос API с действующей сессией заново выставляет cookie
trk_sidсexpiresчерез 30 дней (скользящее окно), поэтому пока вы пользуетесь трекером, вход не слетает. Файлы сессий лежат на сервере вfiles/sessions/(закрыта), системная очистка хостинга их не удаляет. Запросы по ключу cookie не выставляют. - Вход по ссылке (страница, не API).
GET <папка трекера>/?key=<пароль или ключ user>— проверяет секрет, создаёт сессию (какPOST api/login) и отвечает302на тот же адрес безkey(остальные параметры остаются; якорь#epic-65браузер сохраняет сам). Ключclaudeи неверный секрет входа не дают: редирект выполняется так же, и откроется обычная форма пароля. Лимит попыток общий с формой входа (10 неудач за 15 минут с одного IP). Ответ сReferrer-Policy: no-referrerиCache-Control: no-store. Пример:https://it-here-friend.ru/task_tracker/?key=<ключ>#epic-65. Осторожно: ссылка с секретом — это ключ от трекера (попадает в закладки, историю ввода и журнал сервера); лучше использовать ключuser(его можно заменить, не меняя пароль), а не пароль. - Вход перебором закрыт: 10 неудачных попыток с одного IP за 15 минут →
429.
Объекты
Эпик (epic)
| Поле | Тип | Описание |
|---|---|---|
id | int | номер |
title | string | название |
description | string | описание (первая строка обычно «Исходники: …») |
rules | string | правила проекта (копируются из «Правил по умолчанию», потом правятся) |
workdir | string | папка проекта |
tags | string | ключевые слова для поиска |
status | string | todo, on work, test, deploy, test_outside, ready, done |
archived | 0/1 | в архиве |
created_at, updated_at | string | даты |
Задача (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 | /logout | user | выход |
| 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) |
| GET | file.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": []
}
next— в какие статусы можно перейти из текущего (для архивных пусто).open_questions— открытые вопросы всего эпика (включая задачи и баги),notes— заметки.comments,files,logна верхнем уровне — всё по эпику; внутри каждой задачи/бага — только её.
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": "Проверено локально, автотесты зелёные"
}
actorв теле не нужен (берётся из ключа).commentобязателен при возврате назад и записывается комментарием вида[test → on work] причина.- Ответ
200— объект целиком (для эпика — эпик целиком). - Нарушение правил —
409и текст причины, например:
{
"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 | текст (обязательно) |
kind | comment (по умолчанию), note, question |
parent_id | ответ на комментарий; ответ на вопрос закрывает его (resolved = 1) |
options | массив строк (до 8) — только для вопроса Claude: в Telegram придут кнопки-варианты, нажатие записывается как ответ |
force | true — намеренно повторить вопрос, похожий на уже заданный |
- Вопросы от
claudeпроверяются на повтор: если по проекту (эпики с тем жеworkdir) уже есть похожий вопрос, ответ409с номером вопроса и его ответом. Повтор разрешается только сforce: true. - Вопрос от
claudeавтоматически отправляется пользователю в Telegram (если бот включён); эпик с открытыми вопросами нельзя перевести изtodoвon work.
Ответ 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_id | 200 ok, действие не выполняется |
сообщение не из allowed_chat_ids | 200 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 | кто пишет | что это |
|---|---|---|
user | user | сообщение человека; state: pending (ждёт моста) → taken (мост забрал) |
assistant | claude | ответ Claude (может дописываться по частям — стриминг) |
tool | claude | строка вызова инструмента; meta: tool, status (ок/ошибка); в body — команда и результат |
system | claude | служебная строка (например, ошибка запуска) |
permission | claude | карточка запроса разрешения; 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/pending | claude | чаты с новыми сообщениями пользователя (для моста) |
| 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}/take | claude | забрать сообщение пользователя (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/status | user | открыт ли раздел, сколько секунд осталось, задан ли PIN |
| POST | /secrets/unlock | user | ввести PIN: {"pin": "…"} |
| POST | /secrets/lock | user | закрыть раздел |
| GET | /secrets | user + PIN | все записи |
| POST | /secrets | user + 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.
| Из | Можно в |
|---|---|
todo | on work |
on work | test |
test | deploy, ready, on work |
deploy | test_outside, on work |
test_outside | ready, on work |
ready | done, on work |
done | on work |
Дополнительно (сервер отвечает 409 с причиной):
- назад — только в
on workи только с комментарием; doneставит толькоuser; эпик изready/doneдвигает толькоuser;- в
on workодновременно одна задача или баг на эпик; - эпик
todo → on work: есть хотя бы одна задача и нет открытых вопросов; - эпик идёт вперёд в статус X, только когда все задачи и баги в X или дальше (эпик не обгоняет задачи);
- задачи и баги идут вперёд, пока эпик в
on work/test/deploy/test_outside; вернуть задачу вon workможно, только пока эпик вon work; - новые задачи и баги — только в эпик
todoилиon work; архивный эпик не меняется.
Коды ошибок
| Код | Когда |
|---|---|
| 401 | нет ключа/сессии, неверный ключ или пароль |
| 403 | actor/author не совпадает с ключом; нет заголовка X-Requested-With при работе по сессии; неверный секрет webhook |
| 404 | нет такого маршрута, эпика, задачи, настройки |
| 409 | запрещённый переход статуса, архивный эпик, повторный вопрос, закрытые двери по правилам выше |
| 422 | неверные данные (пустое название, неверный тип поля, битый JSON, файл не получен или больше лимита) |
| 429 | слишком много неудачных входов |
| 500 | внутренняя ошибка (подробности — в логе PHP на сервере) |