Трекер-веб: фронт и общая логика
Дубль локального таск-трекера (FastAPI + SQLite) на PHP 8.0+ и MySQL для размещения в сети: веб-страница и API для человека и Claude, плюс Telegram-бот (уведомления, команды, кнопки подтверждения) и скрипт-диспетчер, который будит Claude Code при новых записях. Подробности по маршрутам — в API.md.
Содержание
- Telegram-бот
- Диспетчер для Claude
- Потоки данных
- Ключевые решения и почему
- Установка
- Тесты
- Что заливать на сервер
Зачем и как всё устроено
Идея: Claude Code живёт на отдельном сервере (VPS) и работает не в диалоге, а по очереди поручений. Человек пишет задачу или комментарий в трекере (в браузере или в Telegram), Claude подхватывает, работает, отвечает комментариями и вопросами в том же трекере, а в Telegram приходят уведомления.
человек PHP-хостинг VPS (Claude Code)
┌────────┐ браузер ┌──────────────────────────┐ HTTPS+ключ ┌──────────────────┐
│ браузер├─────────────►│ index.php + api.php │◄───────────────┤ dispatcher.py │
│ │ │ MySQL (эпики, задачи…) │ │ раз в минуту → │
│Telegram├─────────────►│ bot/webhook.php │ │ claude -p … │
└───▲────┘ сообщения │ bot/notify.php ──────────┼──► Telegram └──────────────────┘
└───────────────────┴──────────────────────────┘ уведомления
Трекер и бот стоят на обычном хостинге с HTTPS (всегда доступны, не зависят от VPS), Claude с диспетчером — на VPS. Связь только по API с ключом, поэтому позже всё можно перенести на один сервер, поменяв один адрес в конфиге диспетчера.
Структура файлов
index.php страница (HTML-каркас; версия css/js по времени файла)
api.php единая точка API: авторизация, транзакция, разбор маршрута
file.php скачивание вложений (после проверки входа)
install.php создание таблиц (php install.php или install.php?key=…)
router.php только для локального встроенного сервера PHP
config.example.php образец настроек; config.php — настоящий (в git не попадает)
.htaccess api/… → api.php, закрыт config.php, заголовок Authorization
lib/
db.php PDO, помощники q/rows/one/val/insert/tx, время now()
schema.php схема таблиц и правила по умолчанию
core.php вся бизнес-логика (эпики, задачи, комментарии, поиск, файлы)
chat.php чат с Claude: чаты, сообщения, разрешения
secrets.php секреты: PIN, список, правка (только user)
rules.php правила переходов статусов
dedupe.php защита от повторных вопросов
auth.php ключи, сессия, CSRF, защита от перебора пароля
routes.php таблица маршрутов API
static/
app.js, app.css интерфейс (ванильный JS, без библиотек и CDN), режимы «Трекер/Чат»
chat.js режим «Чат»: список, переписка, стриминг, карточки разрешений
secrets.js режим «Секреты»: PIN, список по проектам, правка
cookie-consent.js плашка согласия на cookie
bot/
Telegram.php универсальный клиент Bot API (один файл, без зависимостей — копируется в другие проекты)
notify.php клиент из config, карточки, уведомления о событиях
commands.php команды и кнопки бота
webhook.php приёмник Telegram
set_webhook.php регистрация приёмника, меню команд, статус
poll.php приём опросом для локальной проверки
tg_cli.php консольная отправка: me / send / updates
dispatcher/ dispatcher.py (будит Claude по inbox), chat_bridge.py (мост чата), fake_claude.py, config.example.json, chat_bridge.config.example.json, тесты
tools/ make_secrets.php, export_sqlite.py + import.php, live_check.php, build_docs.py
tests/ run.php (автотесты), config.test.php
docs/ эта документация
files/ вложения (закрыта от прямого доступа)
Вся логика лежит в lib/core.php и не знает про HTTP: API (api.php) и бот (bot/commands.php) вызывают одни и те же функции. Поэтому правила статусов, дедупликация вопросов и запреты работают одинаково и в вебе, и в Telegram.
Веб-страница
Один экран с тремя зонами; приложение прячется за входом по паролю.
- Вход. Страница при загрузке спрашивает
api/me; если401— показывает форму пароля. После входа в cookie сессии (HttpOnly,SameSite=Strict). Кнопка «выйти» — вверху слева. - Левая колонка. Поиск (в том числе по архиву, работает по эпикам, задачам, багам и комментариям), вкладки «Активные/Архив», список эпиков (статус,
задачи выполнено/всего, открытые баги, теги), «+ Новый эпик», «Правила по умолчанию». - Эпик. Заголовок и кнопки перехода статуса (показываются только разрешённые), «Редактировать», «В архив» (для эпика в любом статусе; если эпик не принят — с подтверждением), для эпика в
todoи для архивного — красная кнопка «Удалить навсегда» (подтверждение показывает, сколько задач, багов, комментариев и вложений пропадёт; отменить нельзя). В архивном эпике — «Разархивировать» и «Удалить навсегда». Ниже: описание, правила проекта (сворачиваются), жёлтый блок открытых вопросов с формой ответа, задачи и баги, заметки, обсуждение эпика. - Список / Доска. Переключатель запоминается в браузере. Доска — колонки по статусам; карточки задач и багов перетаскиваются (это смена статуса от имени пользователя, сервер не пустит запрещённый переход и объяснит причину); клик по карточке открывает её в большом окне (описание, история статусов, комментарии, файлы, редактирование).
- Комментарии. Три вида: комментарий, заметка, вопрос. Ответ на вопрос закрывает его. Можно прикрепить файл.
- Автообновление. Раз в 10 секунд подтягиваются изменения (например, сделанные Claude), если пользователь ничего не вводит и окно не открыто.
- Cookie. Плашка согласия по стандарту проекта: «Подтверждаю», выбор запоминается (cookie + localStorage).
- Телефон. Верстка без горизонтальной прокрутки на 375 px; нажимаемые элементы без синей заливки и рамок при касании (стандарт
web-ui.md), рамка фокуса только для клавиатуры.
Telegram-бот
Бот отвечает только чатам из telegram.allowed_chat_ids (остальным — молчание) и дублирует базовые функции трекера.
Команды
| Команда | Что делает |
|---|---|
/epics | открытые эпики (не done): статус, задач выполнено/всего, сколько в работе, баги, ❓ открытые вопросы; кнопки #N для открытия карточки |
/epic 49 | карточка: статус, описание, задачи со статусами, открытые баги, вопросы; кнопки |
/q | все открытые вопросы — каждый отдельным сообщением; ответ — реплаем |
/new Название | создать эпик (без названия бот спросит следующим сообщением) |
/c 49 текст | комментарий к эпику (без текста — спросит следующим сообщением) |
/find слово | поиск по трекеру (по эпикам) |
/ping | проверка связи: «pong» и время сервера |
/cancel | отменить ожидание ответа |
/help | справка |
Подтверждения и кнопки
- Вопрос от Claude приходит сообщением «❓ Вопрос». Ответить можно реплаем на сообщение. Если Claude передал
options, под сообщением кнопки-варианты: нажатие записывается как ответ. - Эпик дошёл до
ready(это точка приёмки): приходит «✅ Готово к приёмке» с краткой сводкой и кнопками:
- «✅ Принять (done)» — ставит done от имени пользователя; - «↩️ Вернуть в работу» — бот спрашивает причину, следующее сообщение переводит эпик ready → on work и записывает причину комментарием; - «📋 Карточка».
- Карточка эпика: «💬 Комментарий» (текст следующим сообщением) и «🔄 Обновить» (перерисовывает на месте).
- Через бота можно только принять эпик (
done). Остальные переходы делает Claude. - Всё, что пользователь пишет боту (ответ, комментарий, причина возврата), попадает в трекер как запись от
user— то есть во входящие Claude.
Устойчивость
- Приёмник игнорирует повторную доставку того же
update_idи всегда отвечает200. - Если Telegram недоступен или токен неверный, основная работа трекера не страдает: уведомление не доставлено, запись сохранена.
- Уведомления отправляются после успешного коммита в БД (если запрос откатился — сообщения не будет).
Режим «Чат»
Над левой колонкой — переключатель «Трекер / Чат». Режим определяется адресом: #chat, #chat-12 — чат, всё остальное — трекер; последний режим запоминается в браузере.
- Левая колонка чата. «+ Новый чат», вкладки «Активные / Архив», список чатов: название, последняя реплика, цветная точка состояния (серая — свободен, пульсирующая зелёная — Claude работает, жёлтая — ждёт разрешения).
- Окно переписки (справа, на всю ширину). Шапка: название (клик — переименовать), состояние, кнопки «В архив» и «Удалить». Сообщения: человек — справа, Claude — слева (текст с блоками кода), вызовы инструментов — свёрнутые строки (раскрываются: команда и результат), карточки разрешений с кнопками «Разрешить / Отклонить», служебные строки. Внизу поле ввода: на компьютере Enter отправляет (Shift+Enter — перенос), на телефоне Enter — перенос, отправка кнопкой.
- Обновление. Страница опрашивает
GET /chats/{id}?since=…каждые 1,5 с, пока Claude работает, и раз в 6 с в покое — приходят только изменившиеся сообщения; ответ растёт на глазах (стриминг). - Телефон. Список и чат — отдельные экраны (кнопка «←» возвращает к списку); поле ввода учитывает безопасную зону, плашка cookie не перекрывает чат.
- Связь с эпиком. Чат может быть свободным или привязанным к эпику: тогда Claude работает в папке
workdirэпика.
Как отвечает Claude (мост)
dispatcher/chat_bridge.py (только стандартная библиотека Python) запускается рядом с Claude Code — на VPS или на ПК — и работает ключом роли claude:
- Раз в
poll_seconds—GET /chats/pending(обычный HTTP-запрос, модель не тратится). - Забирает сообщения (
take, повтор —409, значит другой проход уже забрал), склеивает их в один промпт и запускаетclaude -p --output-format stream-json --verbose --include-partial-messages --resume <session_id>; промпт передаётся через stdin (кириллица в аргументах Windows ломается),session_idсохраняется в чате — разговор продолжается между запусками. - События потока превращает в сообщения: дельты текста дописываются (
append) в одно сообщениеassistant, вызовы инструментов — отдельные строкиtoolс результатом, ошибки — строкаsystem; в конце чат сноваidle. - Разрешения. Инструмент, на который не хватило прав (
permission_denials), показывается карточкой; после «Разрешить» мост запоминает правило для чата и повторяет запуск с--allowedTools, после «Отклонить» сообщает Claude, что действие запрещено. - Предохранители:
max_parallel,max_runs_per_hour,run_timeout_seconds, списокallowed_toolsиpermission_modeв настройках (chat_bridge.config.example.json).
Мост проверяется без сети и без настоящего Claude: python dispatcher/test_chat_bridge.py (подменный трекер в памяти и fake_claude.py).
Решения и почему
- Опрос, а не WebSocket: хостинг — обычный PHP по FTP, без постоянных процессов; опрос с
sinceдёшев и работает везде. - Мост отдельным процессом, а не вызов Claude из PHP: у хостинга нет доступа к Claude Code и долгих процессов; мост живёт там, где установлен Claude (VPS).
- Разрешения через
permission_denials: Claude в режиме-pне может спросить интерактивно; отказ по правам превращается в карточку, решение человека — в повторный запуск. - Чат личный: только владелец трекера, не сервис для других людей (условия подписки/Agent SDK); вход на сайт — по паролю владельца.
- На сервере CLI должен быть авторизован (
claude→/loginилиsetup-token); без этого мост покажет в чате «Not logged in».
Режим «Секреты»
Третья кнопка переключателя — «Секреты» (адрес #secrets). Раздел открывается только после входа по паролю и ввода PIN-кода (задаётся в config.php, ключ secrets_pin); после ввода доступ живёт 15 минут, потом интерфейс сам возвращается к вводу PIN. Записи сгруппированы по проектам: аннотация (что за реквизиты) и значение; значение скрыто, пока не нажато «показать»; есть «копировать», «изменить», «удалить», «+ Запись» и «Заблокировать». Claude (ключ claude) не имеет доступа ни к одному маршруту раздела; данные лежат в таблице secrets и не участвуют в поиске, журнале и уведомлениях. Хранятся открытым текстом в базе хостинга (как и конфиг с паролями) — защита держится на входе по паролю, PIN-коде и ограничении по ролям.
Диспетчер для Claude
dispatcher/dispatcher.py (только стандартная библиотека Python) работает рядом с Claude Code:
- Раз в минуту
GET inbox— обычный HTTP-запрос, модель не тратится. - Если есть новые записи пользователя — группирует по эпикам и запускает
claude -p "<что появилось>" --output-format jsonв папке проекта (workdirэпика), продолжая прошлую сессию этого эпика (--resume <session_id>); id сессий хранит вstate.json. - После успешного запуска отмечает записи прочитанными (
inbox/ack). Если Claude завершился с ошибкой — записи остаются непрочитанными, а пользователю уходит уведомление в Telegram. - Предохранители: не более
max_runs_per_hourзапусков в час, таймаут запуска, режим--dry-run.
Потоки данных
- Человек → Claude. Комментарий в вебе или сообщение в Telegram → запись в БД (
author = user,seen = 0) →GET inboxдиспетчера →claude -p→ Claude читает эпик, работает, пишет ответ. - Claude → человек. Комментарий/вопрос через API → запись в БД → после коммита
notify_flush()→ Telegram. Смена статуса эпика наready→ сообщение с кнопками приёмки. - Решение человека. Кнопка в Telegram или кнопка в вебе → те же функции
change_status/create_comment→ запись → Claude увидит во входящих. - Вложения. Загружаются через API в папку
files/(закрыта от браузера), отдаются черезfile.phpпосле проверки входа.
Ключевые решения и почему
- PHP + MySQL, без сборки. Деплой — залить файлы по FTP; таблицы создаёт
install.php. Библиотек нет вообще (Telegram.php— свой маленький клиент на curl). - Один API и один набор функций для веба и бота. Правила не расходятся между интерфейсами.
- Актор определяется ключом, а не телом запроса. Claude технически не может выставить
doneили выдать комментарий за пользовательский; ограничения «doneставит только пользователь» соблюдаются сервером. - Два ключа и пароль. Пользователь в браузере — по паролю (сессия), скрипты и Claude — по ключам. Ключи лежат только в
config.php. - Вход не вылетает: 30 дней со скользящим продлением. Раньше cookie была «до закрытия браузера», а серверная сессия жила ~24 минуты и могла быть удалена системной очисткой хостинга. Теперь: cookie и
gc_maxlifetime— 30 дней; файлы сессий — в собственной закрытой папкеfiles/sessions/; каждый запрос API по сессии (страница опрашивает API сама) продлевает cookie ещё на 30 дней (session_refresh()вlib/auth.php). - Вход по ссылке
?key=…. Для закладок и быстрых переходов:index.phpпринимает пароль или ключuserв параметреkey, создаёт сессию и сразу перенаправляет на тот же адрес безkey— секрет не остаётся в адресной строке и истории, якорь (#epic-65) сохраняется. Лимит неудачных попыток общий с формой (10 за 15 минут). Минус: секрет в ссылке попадает в закладки и журнал сервера, поэтому рекомендуется ключuser, а не пароль. - Защита от CSRF без токенов. Запросам по сессии нужен заголовок
X-Requested-With: tracker, который чужая страница выставить не может; запросы по ключу такой защиты не требуют. - Уведомления через очередь после коммита. Не держим транзакцию на время запроса к Telegram и не шлём о том, что откатилось.
- Диспетчер опрашивает API, а не модель. Минутный опрос бесплатен; Claude запускается только при новых записях, поэтому подписка не расходуется впустую.
- Приёмник Telegram отдельно от API и со своим секретом. Telegram не умеет подписывать запросы ключами, поэтому используется
secret_tokenи белый список чатов. - Папка проекта относительная. Все ссылки в странице относительные — её можно положить в любую папку любого домена.
Установка
- Создать БД MySQL (отдельную или общую; таблицы с префиксом
trk_). - Скопировать
config.example.phpвconfig.php, вписать доступ к БД,php tools/make_secrets.php "пароль"— сгенерирует хэш пароля и ключи (user, claude, install_key, webhook_secret), вписать их, указать токен бота иallowed_chat_ids. - Залить файлы, открыть
install.php?key=<install_key>(илиphp install.php) — создаются таблицы. - (Если нужен перенос старых данных)
python tools/export_sqlite.py→php tools/import.php tools/dump.json --replace. - Зарегистрировать приёмник:
php bot/set_webhook.php set https://домен/путь/bot/webhook.phpи… commands. Проверить:… info. - На VPS с Claude:
dispatcher/config.json(по образцу), затемpython dispatcher/dispatcher.py(лучше как службу / вtmux).
Локально (Windows): стенд MariaDB D:\CLAUDE\stands\tracker-web\start.bat (порт 3370), сайт — php -S 127.0.0.1:8100 router.php.
Тесты
php tests/run.php— 121 проверка: правила статусов, дедупликация, входящие, поиск, файлы, бот на заглушке Telegram (все команды и кнопки), HTTP-API на встроенном сервере (авторизация, подмена актора, CSRF, перебор пароля, webhook, закрытые папки). Добавьте--live, чтобы дополнительно отправить настоящее сообщение в тестовый чат.python dispatcher/test_dispatcher.py— диспетчер с подменой Claude на скрипт-заглушку.php tools/live_check.php— живая проверка бота в тестовом чате пользователя (с кнопками).
Боевая площадка
Хостинг Timeweb (PHP 8.2, Apache с mod_rewrite, MySQL), сайт it-here-friend.ru. Раскладка папок: корень — лендинг, task_tracker/ — этот трекер (https://it-here-friend.ru/task_tracker/), vml_claude_bot/ — сервис отправки сообщений бота (send.php, отдельный проект VM_CLAUDE_BOT). FTP-пользователь ограничен папкой сайта: выше public_html подняться не может, каждый проект пишет только в свою подпапку. Заливка — python tools/deploy_ftp.py check|plan|push|config|verify (доступы лежат вне проекта, в D:\CLAUDE\deploy; перед перезаписью файла старая версия сохраняется, после заливки ставится git-тег deployed). Приёмник Telegram на этой площадке: https://it-here-friend.ru/task_tracker/bot/webhook.php; он размещён и проверен, но в Telegram не зарегистрирован, пока вы не скажете (регистрация переключит бота с опроса на вебхук).
Что заливать на сервер
Заливать: index.php, api.php, file.php, install.php, .htaccess, lib/, static/, bot/, docs/, files/.htaccess, config.php (создаётся на сервере, не перезаписывать). Не заливать: tests/, tools/ (кроме одноразовых), dispatcher/ (она едет на VPS), router.php, .git, config.example.php — по желанию.