Персональный менеджер задач для повседневных дел и личных проектов.
| Версия ТЗ | 0.80 |
| Дата | 2026-10-10 |
| Статус | На обсуждении |
Продвинутый личный таск-менеджер. Ключевая идея — минимальный трением ввод: задача закидывается в систему в одну строку, без выбора проекта, тегов и приоритета. Система сама делает предварительную детализацию, а пользователь позже утверждает или правит её при разборе.
user_id — один пользователь не видит чужого. Сад (0.58) в интерфейсе отключён, но его данные по-прежнему изолированы.user_id = NULL (наследие однопользовательской версии) и глобальные настройки забираются первым вошедшим пользователем.Сценарий А — веб-интерфейс (основной). Разбор стека входящих, детализация и утверждение задач, работа с проектами, планирование, просмотр дерева задач.
Сценарий Б — ИИ-агент через MCP. Агент от имени пользователя добавляет задачи (например, из переписки или диалога с LLM). Большинство AI-задач выполняется на стороне агента; сервер предоставляет MCP-интерфейс к данным.
raw) и ожидают детализации.При попадании задачи в стек система самостоятельно предлагает черновые метаданные (0.40: только оформление задачи, без планирования):
Планирование ИИ не делает (решение 2026-09-22, «0% попадания»): проект, приоритет и оценку длительности LLM не предлагает — их заполняет пользователь.
Требования:
approved) только после явного действия пользователя.- [ ] / - [x]) в отображаемом описании кликабельны: клик переключает пункт и сохраняет статус в markdown описания.README.md из репозитория (с подписью-ссылкой на источник). Хост не обязан быть GitHub/GitLab — селф-хостед системы ищутся эвристикой (API github/gitlab/gitea + raw-пути web-интерфейсов, дефолтная ветка HEAD); не нашли — остаётся ссылка. Сырой текст в БД не меняется; кэш прочтённого README — 10 минут. Пользователю подсказывается об опции: в форме создания задачи (подпись редактора) и в пустом описании/инлайн-правке на странице задачи. У проекта этого механизма больше нет (0.67): ссылка на репозиторий живёт отдельным полем, а README показывается вторым описанием (см. 3.8).target="_blank", rel="noopener") — PWA не ловит навигацию в своё окно.Тип задачи: каждая задача имеет тип — разовая или регулярная (повторяющаяся; см. 3.5).
Статусы выполнения:
| Статус | Смысл |
|---|---|
to_do — К выполнению |
Принята в работу, ожидает выполнения |
in_progress — В работе |
Выполняется сейчас |
done — Завершено (закрыто) |
Выполнена |
cancelled — Отменено |
Была актуальной, затем перестала быть актуальной. Не равно удалению — задача сохраняется, т.к. может снова стать актуальной |
deferred — Отложено |
На текущий момент не нужна или не в приоритете; возможно, позже это изменится |
cancelled и deferred — обратимы: задача может быть возвращена в to_do.raw → approved) — отдельная плоскость, не статус выполнения: «сырая» задача из стека получает статус только после утверждения.Дедлайны — двух видов:
Прогнозирование времени выполнения — система оценивает длительность задачи (на основе истории завершённых задач; алгоритм уточняется).
Бюджет — опциональный, на задачу; есть не у каждой задачи:
GnCombobox кита. Доработка 0.71: подсказки показываются только под набранное — на фокусе пустого поля вместо простыни из всех тегов стоит подсказка «начните вводить название тега». Внутри поля, справа — крестик сброса: у поиска нет пункта «— любой —», а фильтр ещё и запоминается (3.8), поэтому снять его иначе было нечем; пустой ввод после сброса применяется сразу, без задержки ввода. Доработка 0.73: фильтра по проекту в списке нет — разрез по проектам это раздел «Проекты» (3.8), в общем списке селект только дублировал его (со страницы ушла и загрузка списка проектов — она была нужна лишь этому фильтру). Подписи у поля тега нет (0.73): табы подписаны собой, а у поля есть иконка тега и подсказка; поле при этом стало 300px, как на странице проекта (0.71), иначе имя тега в нём не читалось. Доработка 0.74: фильтр приоритета — один переключатель «Приоритетные» (GnSwitch кита) вместо табов приоритета (они прожили только 0.73): из шести пунктов нужен был ровно один вопрос — «показать главное». Включённый переключатель оставляет задачи выше среднего: градации high и urgent (7-10 по шкале 3.13). Задача без приоритета (very_low, пустое значение) в отсечку не попадает — она и есть самая низкая. Переключатель стоит рядом с полем тега (с 0.75 — в шапке страницы, см. ниже) и выровнен по центру поля: короткий контрол при выравнивании по низу липнул бы к нижнему краю 50-пиксельной коробки. Доработка 0.76: в шапке выровнены и сами элементы друг относительно друга — у кита под полем остаётся margin-bottom 15px у form-group, а над полем 8px у input, из-за чего поле стояло на 3.5px выше центра ряда; в фильтре эти отступы сняты, и обёртка поля совпадает с его коробкой. Крестик сброса отсчитывается от низа обёртки, поэтому до 0.76 «уплывал» к нижнему краю поля на те же 15px.tag у GET /api/tasks (0.70; регистронезависимо, служебные знаки % и _ во вводе экранируются — «100%» не превращается в маску) и отсечку по нижней границе приоритета параметром priority_min (0.74; целое 0-10, вне шкалы — 422), которой работает переключатель «Приоритетные» (priority_min=7). Параметры по id/градации (tag_id, priority) остаются — их держат тесты и старые клиенты: MCP-инструмент list_tasks фильтрует по тегу своей выборкой в БД, REST-параметр не использует. Задачи без приоритета (NULL) отсечку priority_min не проходят.GnPageHeader) одной группой; отдельной строкой под шапкой остались только табы статуса. На узком экране группа переносится по строкам сама (переключатель строкой, поле — следующей).parent_task_id) в модели данных сохраняется.timeAgo, что у свежести проекта, 0.69); полная дата — в подсказке. Это контекст, а не метаданные, поэтому не бейдж, а приглушённый текст; на странице задачи дата добавления как была — в сайдбаре «Даты» в полном формате. С 0.76 живёт в подвале карточки (слот footer китового GnCard), а не в ряду бейджей: подвал отделён чертой 1px и прижат к низу карточки, поэтому в сетке списка он у всех карточек на одной линии. Иконка рядом с текстом идёт с китовым классом normalize — без него глиф иконочного шрифта садится на 1.7px выше середины строки.ph-folder-dotted), счётчики в заголовках — по всему списку; заголовки секций показываются только когда пустые проекты есть, иначе список выглядит как раньше. Порядок применяется до пагинации (3.6), чтобы секция не разъезжалась по страницам.100%/M × N), а не процентом кита: у процента округление до целого, и на границе клетки оставалась бы щель. Трек на карточке тоньше китовских 18px — 8px (0.68): полоса здесь подложка под счётчиком, а не самостоятельный элемент. Проект закрыт целиком — полоса и счётчик уходят в зелёный кита. Цвет проекта в тексте не используется (тёмная ступень цветных рядов и тёмный край нейтральной шкалы, 0.72 — нечитаемы как текст) — им закрашена только полоса.<a>: клик в любом месте, фокус с клавиатуры, «открыть в новой вкладке»), заголовок внутри — не ссылка (вложенные ссылки невалидны). В архиве (3.8.1) кнопка «Открыть» остаётся — там рядом действия восстановления/удаления.#rrggbb (палитра — представление фронта, бэкенд проверяет только формат, поэтому цвет из MCP вне палитры тоже валиден и показывается отдельной клеткой). Показывается в трёх местах: карточка в списке проектов, шапка страницы проекта, метка проекта у задач (списки, дашборд, страница задачи). В фильтрах и селектах проектов цвета нет: в выпадашке это шум, а не узнаваемость. Базовая палитра уточнена в 0.69: ряд «песочного» (warning кита, оттенок 35°) заменён своим жёлтым (#e5cf5a, оттенок 50°) — рядом с оранжевым (25°) он читался вторым оранжевым, а не жёлтым; жёлтого тона в ките нет вовсе, поэтому база своя. Добавлен ряд нейтральных от белого — он последний в сетке: цветные метки узнаются быстрее, а серые нужны редко и на своём месте не мешают. Палитра пересобрана в 0.72 по замечанию «слишком одинаковые цвета»: клеток стало меньше, но любые две клетки сетки различимы. Убраны приглушённые варианты (тот же тон с половинной насыщенностью) — именно они и давали «одинаковые» клетки: и рядом со светлой ступенью своего ряда, и с приглушёнными чужих тонов. Из базовых тонов убран голубой #7dcfff (199°): он стоял в 12° от циана и в 20° от синего — три холодных клетки читались одним синим; добавлен коричневый #8c5e35 (28°, светлота 38%, насыщенность 45%) — коричневого тона в ките нет, и это свой цвет, а не «тёмный оранжевый». Ступени считаются от светлоты базового (светлая +18%, тёмная −24%), а не заданы абсолютной шкалой: у тона, стоящего у предела светлоты (36..88%), ступени просто нет, поэтому ряды коричневого и сиреневого короче — это честнее клетки, не отличимой от соседней. Разница измеримая: минимальная попарная разница по CIELAB — ΔE 12.5. Ряд нейтральных сокращён с 12 ступеней до 5 через 16%: от белого #ffffff до #5c5c5c. До чёрного шкала не доходит намеренно: чёрная метка на тёмной карточке кита не видна, то есть была бы выбором без результата (раньше это называлось ценой полноты шкалы).repository_url, отдельно и от заметки, и от сайта. Раньше её клали в заметку, и она блокировала саму заметку (заметка из одной ссылки подменялась README): у проекта теперь два описания — заметка остаётся заметкой, а из репозитория подтягивается и показывается README как дополнительное описание. Механизм распознавания ссылки в заметке проекта снят (у задачи README-вместо-описания остаётся, см. 3.3). README показывается карточкой «README из репозитория» под заметкой: в шапке карточки — ссылка на репозиторий (хост и путь без схемы, открывается в новой вкладке), в теле — отрендеренный Markdown; длинный README сворачивается тем же порогом ~420px (3.3). Тянет его бэкенд-прокси /api/projects/{id}/readme с кэшем 10 минут (данные не покидают VPS — как у фавикона, 3.11): привязка к проекту, а не ?url=, чтобы эндпоинт не стал открытым прокси для чтения произвольных адресов. README в репозитории не нашёлся — тихая подпись «README не найден» (поле необязательное, проект живёт и без него). Правится строкой «Репозиторий» в параметрах проекта и в форме правки. Форма ссылки не проверяется (кроме http(s) с хостом): не похожа на репозиторий — прокси честно ответит «не найден». Существующие заметки-ссылки в БД не переписываются: ссылку нужно указать в новом поле.pinned — важные проекты держатся наверху списка и не уезжают вниз с пагинацией. Закреплённые живут отдельной секцией «Закреплённые» вверху (иконка ph-push-pin, янтарный бейдж-счётчик), независимо от наличия задач: закреплённый проект без задач остаётся в «Закреплённых», а не в «Без задач». Переключателей два, поле одно: булавка в правом верхнем углу карточки списка — рядом с бейджем актуальности (шапка сдвигается левее), видна всегда, в покое не приглушена; и строка «Закреплён» в параметрах страницы проекта (значение кликабельно, как у остальных строк). Место булавки переставлено в 0.67: в правом нижнем углу карточки она читалась частью подвала, а на странице проекта её и вовсе не находили — поэтому у имени проекта появилась кнопка-булавка рядом с названием (в архиве скрыта: страница только для чтения, состояние видно строкой «Закреплён»; строка в параметрах остаётся — второй переключатель). Новый проект создаётся незакреплённым. В MCP — поле pinned у update_project (3.10).created_at или done_at); правки самого проекта (заметка, цвет, статус) активностью не считаются — сортировка про работу над проектом, а не про правку его карточки. Проект без задач стоит в конце своей группы. Порядок применяется до пагинации (3.6), а счётчики секций и показ заголовков считаются по всему списку, не по странице: иначе на второй странице заголовки то появлялись бы, то исчезали.created_at/done_at задач; правки самого проекта не считаются); полная дата — в подсказке. Срок при этом стоит справа — важное в одном и том же углу у всех карточек, независимо от того, есть ли свежесть. Сутки календарные, ниже недели — дни, дальше округление вниз до крупной единицы (45 дней — это уже месяцы, 400 — годы). Появились плюральные формы: их выбирает правило самой локали (Intl.PluralRules) — «2 дня», а не «2 день»; в словаре формы записаны одной строкой через «|» (ru/uk — три, en — две), выбор формы в коде не хардкодится. Число при единственном числе остаётся («1 месяц назад»). У проекта без задач свежести нет — строка пустая, но место подвала сохраняется, чтобы низ карточек в ряду не разъезжался.id не восстанавливается осознанно: имя тега по id известно только после загрузки справочника.Проект, с которым пользователь закончил работу, отправляется в архив — один архив на все проекты, отдельных архивов задач нет.
Анти-прокрастинационный режим: пользователь указывает доступное время в часах (например, «3», можно 0.5), система предлагает подходящие задачи; предложение — те же карточки, что в списке задач.
?via=options) и берёт её в работу на странице задачи; закрытие задачи, открытой из выдачи, помечается «выбрал и сделал».create_task — добавить задачу (достаточно текста; метаданные опциональны);update_task, complete_task;list_tasks — фильтры по проекту (id или имени), тегу, статусу, подстроке;get_task — полное описание с вложениями;list_projects / get_project — обнаружение проектов и восстановление контекста проекта (заметка проекта + его открытые задачи); список отдаётся закреплёнными первыми (0.66), затем по имени — как в UI;create_project — новый проект (имя, опционально заметка, приоритет, color, site_url и repository_url); дубликат имени отклоняется с подсказкой; заметка уходит в фоновую суммаризацию; микронаграда как при создании из UI;update_project — переименование, заметка, relevance_status (active/paused), приоритет, color, site_url, repository_url, pinned (true — закрепить, false — открепить; 0.66); адресация по id или имени;color (#rrggbb), site_url и repository_url (http(s)://host/...) обоих тудов — необязательные поля проекта (3.8); как принято в файле, None — «не менять», а пустая строка убирает значение; невалидный формат отклоняется с подсказкой формата (repository_url — репозиторий, из которого в интерфейсе подтягивается README вторым описанием, 0.67).archive_project / restore_project — архив вместо удаления (0.45): проект прячется вместе с задачами из активных (история сохраняется), restore возвращает; вызовы идемпотентны; архивные проекты видны через list_projects(include_archived=True). Безвозвратного удаления проектов в MCP нет намеренно;list_tags — справочник тегов (id для параметра tag_ids).instructions: порядок работы, правило привязки задач к проекту, восстановление контекста через get_project после перерыва, запрет придумывать id, автоматика наград/спавна при закрытии;project_name (регистронезависимо) в тудах задач (create_task/update_task/list_tasks) и в тудах управления проектами (update_project/archive_project/ restore_project) наряду с project_id;create_task без проекта содержит подсказку.gnt_..., показывается один раз при создании; в БД только sha256-хэш). Там же ссылка на страницу-инструкцию по MCP (/mcp-help): что реализует, адрес сервера (совпадает с адресом приложения), токен, примеры подключения (Claude Code, универсальный JSON).MCP_TOKEN удалена — старые конфиги агентов получат 401, пока владелец не создаст токен в «Настройках». С 0.42 Bearer-токен принимается и на REST /api/* — им ходит браузерное расширение (3.18).update_task/complete_task) идёт по тому же общему пути, что и в UI: спавн регулярной, XP, монеты (3.5, 3.13); в открытых вкладках — скромный тост о награде (3.14). Растение больше не выдаётся — сад отключён в 0.58.projects.summary — краткое резюме заметки (суть + полезные признаки для классификации задач). Суммаризация генерируется той же LLM фоном при создании проекта с заметкой и при правке заметки (кэш в БД); без суммаризации в контекст идёт обрезка заметки./api/projects/{id}/favicon, бэкенд тянет иконку с сайта проекта и кэширует её на диске (7 дней; негативный результат «иконки нет» — 1 час, чтобы не долбить сайт). Показывается в метке проекта (карточка списка и шапка страницы проекта) и в чипе-ссылке на сайт (0.65). Сторонние favicon-сервисы не используются: адрес проекта не должен утекать третьей стороне (принцип «данные не покидают VPS»). Кандидаты: <link rel="icon"> из <head> сайта, затем /favicon.ico, /favicon.png, /apple-touch-icon.png. Тип определяется по magic-bytes (PNG/JPEG/GIF/ICO/WebP); SVG-иконки не принимаются — скриптуемый формат (как запрещённые mime вложений). Ограничения запроса — в 4./api/projects/{id}/readme, бэкенд читает repository_url проекта и тянет сырой README перебором известных путей (см. 3.3), кэшируя прочтённое на 10 минут (в том числе негативный результат — чтобы не долбить git-хост). Привязка к проекту, а не параметр ?url=: открытым прокси для чтения произвольных адресов эндпоинт не становится. Ссылку на репозиторий фронт не передаёт — её знает только бэкенд. По той же причине, что и у фавикона, сторонние сервисы чтения README не используются.profile.locale из SSO-профиля, scope profile).app_settings, ключ language), пустое значение = следовать SSO.Маленькая дополнительная награда за выполнение задач — мотивация без негативных последствий. Принцип только позитив: XP только начисляется, ничего не сгорает и не отнимается; стрики/серии (которые «ломаются» при пропуске) не используются.
xp_events (поле kind разделяет события: task — закрытие, create_task / create_project — создание).task_id = NULL — удаление задачи или проекта награду не отнимает.POST /api/xp/daily — раз в сутки (граница дня UTC) начисляется 50 монет (только монеты, XP нет), повторные визиты в те же сутки молча пропускаются. Тост «+50 монет» только в день первого визита.xp_events — как история для возврата сада./achievements, 0.58): весь видимый прогресс без сада. Бейджи шапки — уровень с названием звания, суммарный XP и монеты; полоса прогресса до следующего уровня; кнопка «Дерево званий»; справка «Как это работает»; ачивки с тирами (см. ниже) — тот же блок, что и в саду (общий компонент, логика не дублируется). Данные — GET /api/tasks, GET /api/xp, GET /api/xp/events; GET /api/garden не вызывается (это и есть механизм отключения выдачи растений, см. ниже). Страница слушает SSE xp.changed и task.changed. Пункт меню — между «Проектами» и «Статистикой» (место прежнего «Сада»), путь /garden редиректит на /achievements (старые ссылки и закладки PWA не ломаются).floor(xp / 2); заголовок X-Earned-Coins рядом с XP в тосте), за создание задачи/проекта и бонусом 50 × L при достижении уровня L (0.48: было 25 × L). Ежедневная награда не менялась (50 монет). Хранение — таблица coin_events (журнал заработка и трат; баланс = сумма amount). Траты — отрицательные события (source = shop); продажи нет («только позитив»). Баланс виден на «Достижениях» — GET /api/xp отдаёт поле coins (0.58). Пока сад отключён, тратить монеты негде: они копятся и пригодятся, когда сад вернётся.xp_events и показываются на «Достижениях» (0.58) — общий блок с садом (components/AchievementsPanel.vue). Описание по клику (0.49): карточка-ачивка — триггер GnPopover кита, клик открывает панель с кратким описанием условия (progress.achHint, i18n ×3; тултип по hover не годится — не работает на тач-экранах).earned_via) и празднуется меткой «выбрал и сделал» + счётчик-ачивкой; XP при этом не меняется.xp_events и дат задач, без новых эндпоинтов.progress.about./garden редиректит на /achievements, код сохранён для возврата (по прецеденту представления «дерево», 3.6). Растения больше не выдаются: GardenItem создаётся лениво — в ensure_garden(), которую зовёт только GET /api/garden; UI этот эндпоинт не вызывает, поэтому выдача прекращается сама. Модель GardenItem, журнал coin_events и накопленная редкость (xp_events.rarity) сохраняются — сад вернётся с прежним состоянием. Всё описанное ниже — исторический блок (как сад устроен), актуальны только XP, уровни, монеты и ачивки.nearest масштабирование без сглаживания, целочисленный «нативный пиксель» (матрицы 16px × 2 = 32px на клетку). Все спрайты (тайлы травы/дикой земли, изгородь, домик, 13 видов растений × 3 стадии, декорации, кольца редкости, блёстки) генерируются из пиксель-матриц прямо в коде — внешних файлов-ассетов нет. Рендерер (src/game/GardenRenderer.ts) — обычный класс, не оборачивается во Vue-reactive (deep-прокси ломает внутренние identity-проверки Pixi); Pixi загружается динамическим импортом (отдельный чанк). Инвентарь, поповер и drag-ghost — DOM поверх канваса.xp_events, визуально — золото + бейдж.garden_items.item_key) — открытые позже виды не перерисовывают старые растения. Растение привязано к событию XP (переживает удаление задачи).garden_items.x/y = NULL) — место покупки всегда видно пользователю. Из инвентаря декорация перетаскивается на карту; перетаскивание размещённой декорации обратно на полосу инвентаря убирает её с карты (растения в инвентарь вернуть нельзя — сад только растёт).Данные меняются не только из UI: MCP-агенты (3.10) и фоновая автодетализация (3.2) пишут в БД вне HTTP-запроса. Все виды обновляются событиями от сервера — без обновления страницы и переходов между страницами.
GET /api/events (авторизация штатная, 401 — на вход). Транспорт — SSE (text/event-stream), по одному соединению на вкладку (EventSource, авто-reconnect браузером). Keep-alive: при тишине дольше 20 с сервер шлёт кадр ping, чтобы прокси (Vite dev, nginx) не рвал поток.kind + опциональные данные; фронт реагирует refetch'ем (перечитывает данные целиком), а не патчами. Трейлинг-debounce 300 мс коалесцирует бёрсты (закрытие задачи = 2–3 события). Виды подписываются на свои kind'ы: стек/список — task.changed, task.deleted, detail.changed, project.changed, project.deleted; статистика и «Достижения» — xp.changed, task.changed; сад (код сохранён, страница отключена в 0.58) — garden.changed, xp.changed, task.changed (рефetch подавляется на время drag по сцене); страница задачи — перечитывается при task.changed/detail.changed своего id. garden.changed продолжает публиковаться, но слушателей в UI больше нет — событие безвредно (0.58).task.changed {id}, task.deleted {id}, detail.changed {id}, project.changed {id}, project.deleted {id}, garden.changed {reason}, xp.changed {amount?, celebrate}, settings.changed, ready (первый кадр), ping. Упрощение: закрытие задачи через MCP не шлёт garden.changed — refetch по task.changed покрывает (сад отключён в 0.58, слушателей у события нет).X-Earned-XP, ТЗ 3.13); SSE-эхо и эхо соседней вкладки молчат — окно активности 3 с (localStorage). Соседние вкладки показывают скромный тост «+N XP» без конфетти.Приложение устанавливается на устройство как приложение (Chrome/Android; вопрос оффлайн-режима данных — открытый вопрос 8.4, пока приложение всегда онлайн).
#16161e — фон и theme-color, чтобы не мигало белым при запуске и не зеленела шапка окна), иконки 192/512 + maskable-512, id/start_url = /, язык ru.autoUpdate): precache оболочки — собранные ассеты, шрифты/иконки кита (~6 МБ лимит), /api/*, /auth/*, /mcp/* никогда не кэшируются и не получают fallback на index.html.autoUpdate); SSE-соединение при обновлении рвётся — reconnect покрывает (3.14).devOptions выключен) — PWA проверяется на прод-сборке (dist), не на dev-сервере.hover:none, pointer:coarse) — иначе iOS зумит страницу при фокусе и зум застревает;100dvh вместо 100vh у drawer'ов, мобильного меню и модалок (низ уезжал под панель Safari и индикатор «домой»); на старых Safari остаётся 100vh;apple-mobile-web-app-* меты: standalone для «На экран Домой» (до iOS 16.4 манифест не читается), black-translucent статус-бар, короткий тайтл;:active-состояния);POST /auth/webhook (контракт — docs/04-events-and-webhooks.md в репозитории gnexus-auth): HMAC-SHA256 заголовков X-GNexus-*, секрет — GAUTH_WEBHOOK_SECRET (создаётся/ротируется в админке SSO, client.webhook/rotate-secret); секрет не задан — приём выключен (503).user.email_changed, user.profile_updated — синхронизация профиля User (email, name ← profile.display_name, locale, avatar_url) без повторного логина; ключа нет в profile — поле не менялось, не трогаем;user.blocked, user.archived, user.deleted — отзыв всех MCP-токенов пользователя (агент теряет доступ вместе с аккаунтом)./auth/me отдаёт свежие данные из БД, а не из cookie-сессии, поэтому изменения профиля от webhook видны сразу (без повторного логина).webhook.test и будущие) подтверждаются 200 без действий — чтобы SSO не гонял retry'и по незнакомому типу; при недействительной подписи/битом payload — 400./login вне навигационной оболочки: карточка кита (GnCard, 460px) с логотипом, названием и единственной кнопкой «Войти» — инициация SSO-флоу (/auth/login с return_to). Пока /auth/me не ответил, в карточке только лого и лоадер — кнопка не вспыхивает перед редиректом уже залогиненного; тот же вид, что на экране входа gnexus-creds./auth/me → 401); после логина возвращается на исходный адрес. Истёкшая сессия в API/SSE-коде тоже ведёт на /login, а не на бэкендовый SSO-флоу./auth/logout после очистки сессии редиректит на /login (не оставляет пользователя на JSON-ответе)./auth/me не считается «не залогинен» — оффлайн-оболочка открывается без редиректа.Расширение для Chrome/Firefox (MV3): клик по иконке в трее браузера → попап с двумя вкладками — «Добавить» и «В работе». Быстрый захват входящих (3.1) вне страницы сервиса.
/api/tasks + PATCH project_id, как в веб-форме). Фидбек — анимация-галочка и тост «Задача добавлена (+N XP)»; поле очищается, выбранный проект сохраняется; кнопка «Открыть GNexus Tasks» ведёт на главную.in_progress; название, чип проекта, метка просрочки, теги); клик открывает задачу в сервисе, действий в попапе нет./api/* с Authorization: Bearer <MCP-токен>. Токен вставляется один раз из «Настроек → Токены MCP», хранится в chrome.storage.local; отзыв токена отключает расширение (как и агентов, 3.10).http://localhost:15134; произвольный адрес задаётся в настройках попапа и запрашивает host-разрешение (optional_host_permissions + chrome.permissions.request).chrome.storage.local и восстанавливается при следующем открытии; после успешной отправки текст очищается (как в веб-форме)./extension): скачивание ZIP-сборки расширения (раздаётся статикой фронта; в Docker-образе фронта собирается из extension/), шаги загрузки unpacked и подключения токена. Из установленного PWA пункт меню скрыт (display-mode: standalone) — по прямому адресу страница показывает пояснение.--gn-border-color-muted, мелкие контролы/инсеты — 3px; темы Verdant/Ember/Mono и дизайн-токены --gn-* доступны, но не используются пока в gntodo)..nav-drawer-body: тонкая, бегунок акцентом #7aa2f7 по фону панели), всё остальное берёт глобальное правило кита: 10px, дорожка #16161e, бегунок #414868, кнопок нет. В 0.76 полосы были переведены на «китовый язык» целиком — тонкие, с акцентным бегунком и прозрачной дорожкой; правка отменена: акцентный бегунок читался поверх шапки, а прозрачная дорожка снимала с полосы её служебный вид. Теперь толщина и цвета китовые, добавлено только скругление бегунка. scrollbar-width и scrollbar-color намеренно не задаются: как только они выставлены, Chromium игнорирует ::-webkit-scrollbar-* целиком, и скругление пропадает; у .tabs-list и .nav-drawer-body они стоят у кита — там полосы остаются как были.footer кита (0.76): кит рендерит его как <footer class="card-footer"> с одними отступами, а раскладку, размер шрифта и цвет (flex / 13px / #a9b1d6) задаёт только горизонтальному варианту карточки — вертикальному их даём сами. Содержимое карточки кит тянет на height:100%: с подвалом оно вытолкнуло бы его за карточку (в сетке списка у карточки height:100%, а overflow:hidden кита в приложении снят), поэтому карточка задачи раскладывается колонкой, а остаток высоты отдаётся содержимому — подвал прижат к низу и у карточек одного ряда стоит на одной линии.documentElement.scrollWidth был 533 при вьюпорте 390. Причина в сетке страницы задачи: мобильное правило задавало grid-template-columns: 1fr, а голый 1fr — это minmax(auto, 1fr), и трек не сжимается ниже min-content содержимого (517px в коробке 358px). Теперь трек — minmax(0, 1fr), а колонки-элементы получили min-width: 0. Вторая причина того же симптома — длинные неразрывные токены в описании (URL, инлайновый code вида GAUTH_REDIRECT_URI=https://…): абзац не помещался в узкую карточку, и md-view со overflow-x: auto честно скроллил его по горизонтали (коробка 324px, содержимое 483px). Теперь переносим токен по символам (overflow-wrap: break-word — именно break-word, а не anywhere: intrinsic- размеры не меняются, поэтому таблицы и фенсед-блоки по-прежнему прокручиваются, а не сжимаются; pre с white-space: pre перенос не подхватывает). На широком экране переносить нечего — десктопная раскладка не изменилась..dropdown-menu от левого края триггера (position:absolute; left:0, min-width:220px), поэтому у триггеров у правого края экрана меню уезжало за вьюпорт, и приложение выравнивало координаты инлайном уже после открытия — на телефоне это давало постоянные «съехавшие» меню. До 767px меню вместо выпадашки становится шторкой снизу: панель во всю ширину экрана, прижатая к низу, со скруглением верхних углов 12px, «ручкой»-грабером, пунктами высотой 44px (китовые 34px мелковаты под палец) и прокруткой при длинном списке; появление — выездом снизу (0.22s), а не китовым overlay_reveal. Экран под шторкой затемняется (слой #16161e с прозрачностью 0.72), тап по затемнению закрывает меню и не нажимает то, что под ним. Затемняющий слой лежит вне #app (в index.html) и показывается по :has(.dropdown.is-open): для китового обработчика клик по нему — «вне меню», поэтому закрытие работает без JS, а выравнивание координат на мобильных выключено за ненадобностью. Китовый panel_boot оставляет у шапки страницы transform, а это содержащий блок для position:fixed — на мобильных анимация шапки снята, иначе шторка из шапки раскладывалась бы по коробке шапки, а не по экрану; правило «меню в шапке прижато вправо» тоже оставлено десктопу, на мобильных оно подрезало бы шторку справа. Правка задумана как временная: та же задача оформлена для gnexus-ui-kit (там её место — в самом GnDropdown), при переходе на реализацию кита приложение удаляет свой CSS-блок, слой в index.html и мобильную ветку в dropdownClamp.ts..app-content .page-header) держала position: relative; z-index: 5 — правило нужно десктопу (китовый panel_boot оставляет transform, и без z-index карточки рисуются поверх выпадашки), но z-index создаёт контекст наложения. Внутри него заперта fixed-шторка: слой затемнения (вне #app, z-index 1090) ложился поверх шторки и перехватывал тапы — меню из шапки на странице задачи открывалось затемнённым, а клик по пункту лишь закрывал его. Теперь position: relative и z-index включены только с 768px; на мобильных шапка контекста не создаёт, шторка попадает в корневой контекст и лежит выше затемнения.dropdownClamp.ts) ставило меню инлайновый right: 0, но китовый .dropdown-menu{left: 0} при этом продолжал действовать — оба края заданы, и при min-width: 220px меню оставалось у левого края триггера: у правой колонки карточек (три колонки на 1440px) оно выезжало за вьюпорт на всю свою ширину (right 1593 при 1440). Теперь при развороте влево выставляется и left: auto.ph-dots-three-outline — как в карточках задач, стека и на странице проекта (на странице задачи стоял залитый ph-dots-three)..tabs-list{overflow-x:auto}), но видимая полоса тут только шумит — обрезанный шестой таб и так читается как «дальше есть». Прокрутка сохранена, спрятана только полоса (scrollbar-width: none + ::-webkit-scrollbar для Safari).left: -5px от коробки контрола, поэтому видимый левый край свитча выходил на 3–5px левее соседей по ряду. Сдвиг — компенсация выступа, а не отступ между элементами.!important-войн с китом: только каскадное переопределение поверх публичных классов.--gn-surface-page) — приём логотипа gnexus-creds: знак читается и как тёмная плитка на светлой панели вкладок, и на тёмных поверхностях кита. Знак занимает ~56% холста (в сетке 64 — 36×35, поля ~14): в 0.56 знак стоял в прежней сетке (62.5%), в 0.57 уменьшен на 10% — scale(.9) вокруг центра знака (32, 32.5) в logo.svg; пропорции строк не меняются. logo.svg (шапка, экран входа, попап расширения) и favicon icon.svg — один и тот же файл (тоже как у creds). PWA: icon-192/512 (any) и maskable-512 — холст 80% кадра (safe zone адаптивных иконок Android). PNG расширения 16/32/48/128 и все PWA-иконки растеризуются из logo.svg скриптом tools/rasterize_icons.sh: фон --gn-surface-page впечатан, потому что прозрачность iOS рисует чёрным (углы холста скруглены, за ними — ничего). До 0.54 знак был в рамке-чипе, в 0.54 рамку сняли с подрезкой viewBox по глифу — знак занимал всю отведённую коробку, но в шапке и на экране входа выглядел «голым»; с 0.56 знак подан на холсте. Попап расширения показывает лого из своего public/ (у упакованного расширения свой корень — ссылка на фронт не работает), поэтому с 0.59 копию обновляет тот же tools/rasterize_icons.sh: одна команда на правку логотипа.card-title (0.59): card-title — имя шапки карточки в ките (header.card-title, у него padding: 15px 15px 0); тот же класс на вложенной ссылке давал вторые 15px, и текст заголовка уезжал вправо от остального содержимого. Ссылка стека носит собственный card-link.GnChip/GnChipGroup/GnPopover/GnDropdown/ GnRadioGroup/GnRange цвета не принимают), а GnPopover оборачивает содержимое в <p class="popover-text"> — сетку свотчей туда не положить. Панель выбора — свой блок с поверхностью/рамкой/радиусом кита (как .popover-panel), свотчи — отдельные кнопки с aria-pressed/aria-label и тонкой внутренней обводкой (0.72: без неё тёмные ступени сливаются с поверхностью панели); закрытие по клику вне и Escape. Палитра (27 оттенков) строится детерминированно из 8 базовых тонов: шесть — цвета кита (error #f7768e, accent #ff9e64, success #9ece6a, info #bb9af7, secondary #7aa2f7 и циан #2ac3de из палитры темы — своего токена у него нет), два — свои (жёлтый 0.69, коричневый 0.72: таких тонов в ките нет). От базового тона — светлая и тёмная ступени, посчитанные от его светлоты, — «базовый, светлый, тёмный». Ступень, которую зажал предел светлоты (36..88%), в сетку не берётся, поэтому ряды коричневого и сиреневого короче; тёмная ступень берётся с насыщенностью ×0.8: при низкой светлоте полная насыщенность читается на тёмной подложке как неон. Приглушённых вариантов (половинная насыщенность) и отсева «почти одинаковых» клеток по каналам (0.63..0.71, сетка 100 клеток рядами по 12..13) больше нет: в 0.72 ступень отведена от базового не меньше чем на 14% светлоты, а любые две клетки сетки — на ΔE 12.5 по CIELAB, и сетка сократилась почти вчетверо (100 → 27). Нейтральный ряд (0.69) выведен не из базового цвета — у серого оттенка нет, поэтому ступени заданы своей шкалой светлоты (в 0.72 — 5 ступеней через 16%, от #ffffff до #5c5c5c) и рисуются равными каналами, минуя общий зажим светлоты цветных рядов.span, ссылки среди бейджей нет, поэтому чип сайта — сырой <a> в геометрии бейджа кита (24px/радиус 3px/13px/600), капс не берём. Тайтл карточки крупнее: размер поднимается правилом :deep(.card-title) — в ките .card .card-title размера шрифта не задаёт (14px приходят наследованием от body), поэтому правило ничего не «перебивает». Метка проекта (кружок + фавикон) — свой компонент ProjectMark; размеры в em от кегля имени: в карточке база — тайтл (16px), в шапке страницы задаётся 18px. Кружок внутри метки крупнее одиночного (ProjectColorDot в бейджах задач и дашборда оставлен прежним) — селектор потомка бьёт правило дочернего компонента без !important. Кольца у фавикона нет (тонкая обводка --gn-border-color-muted снята в 0.65 по просьбе пользователя): край иконки и без неё читается на тёмном фоне. Бейдж проекта — ссылка (0.75): сам бейдж остаётся китовым span, клик и подсветку даёт RouterLink вокруг него (вложенная в <span> ссылка валидна). .badge в scoped-стилях матчится без :deep — это корень дочернего GnBadge, он несёт scope-атрибут родителя.@click висит на корне GnCard (кит раскрывает attrs на <article class="card">), а обработчик пропускает клики, у которых event.target.closest('a, button, input, select, textarea, label') что-то нашёл, и клики по выделенному тексту (window.getSelection() непустой — иначе сниппет не скопировать).width/height: .65em, border-radius: 50%) в четырёх местах (карточка списка, шапка проекта, бейдж проекта у задачи, строки проектов дашборда) — иначе стиль дублировался бы по файлам. Нет цвета — нет и кружка. Отступ справа 0.25em (0.73): кружок всегда стоит вплотную к имени проекта — в бейдже задачи он прилипал к нему. Метка ProjectMark этот отступ снимает (там зазор задаёт flex-gap), поэтому интервал перед фавиконом не удваивается.GnProgress — одна заливка, GnProgressStages — сетка этапов), поэтому карточка проекта рисует клетки насечками поверх трека (::after трека с repeating-linear-gradient, шаг 100%/M), а сама полоса остаётся GnProgress — с ролью progressbar, aria-valuenow/max и анимацией ширины. Переменные оформления (--card-color, --task-cells, --done-cells) вешаются на корень карточки, а не на полосу: кит в GnProgress ставит свой style после ...attrs, и переданный извне style теряется.| Требование | Значение |
|---|---|
| Архитектура | Клиент-серверное; SPA + REST API + MCP |
| Фронтенд | SPA на Vue 3 с gnexus-ui-kit (npm: gnexus-ui-kit@^0.4.0, peer: Vue ^3.4) |
| Бэкенд | Python (FastAPI — предположительно), клиент SSO — gnexus-gauth |
| СУБД | PostgreSQL (рекомендация для VPS) |
| Авторизация | SSO через auth.gnexus.space, библиотека gnexus-gauth (https://git.gnexus.space/git/root/gnexus-auth-client-py.git) |
| Хостинг | VPS, HTTPS |
| AI | Ollama на внешнем сервере (адрес и модель — конфигом, OLLAMA_BASE_URL/OLLAMA_MODEL); контейнера ollama в compose нет |
| Упаковка | В итоге всё пакуется в Docker (docker-compose: API, PostgreSQL, фронт; TLS — внешний reverse-proxy) |
| Мобильность | На старте — только PWA; Android-приложение — вне скоупа стартовой версии (вернуться к нему позже) |
| Ширина контента | До 1440px на десктопе (0.68; было 1200px): контейнер контента тянется по ширине окна и упирается в 1440px, дальше центрируется с полями; на этой ширине списки задач и проектов раскладываются по три в ряд (3.6, 3.8) |
| Приватность | Все данные и LLM-вызовы — на собственном сервере |
| Реактивность | SSE (/api/events): грубозернистые события + refetch, см. 3.14 |
| PWA | манифест + SW (precache оболочки, autoUpdate), оффлайн-оболочка без кэша API, см. 3.15 |
Внешние зависимости проекта (не форкать, обновлять через пакетный менеджер):
gnexus-ui-kit — часто обновляется, использовать только публичный API кита;gnexus-gauth — клиентская библиотека SSO.Исходящие запросы за иконкой сайта (0.63, 3.11) — единственный случай, когда бэкенд ходит в интернет по адресу, заданному пользователем (в т.ч. агентом через MCP), поэтому запрос ограничен барьером SSRF:
ipaddress.is_global) — private/loopback/link-local/reserved/CGNAT (включая метаданные облака 169.254.169.254) отклоняются;content-type обязан быть изображением, а расширение определяется по magic-bytes;X-Content-Type-Options: nosniff и Cache-Control: private, без возможности исполниться как документ.Остаточный риск: между проверкой хоста и соединением HTTP-клиент резолвит имя повторно (DNS-rebinding, TOCTOU); полное закрытие требует соединения по IP с подменой Host/SNI и ломает проверку TLS-сертификата. Для личного приложения риск принят — основной вектор (публичный URL → метаданные облака) закрыт.
Мультиюзерность (с 0.34): на задачах, проектах, тегах, документах, XP/монетах, саде и настройках есть user_id (nullable — NULL забирается первым вошедшим, см. 1.2); все запросы API/сервисов/MCP-тулов скоупятся по нему. Модель сада (0.58) сохранена без изменений — раздел отключён, данные ждут возврата; XP/монеты и rarity продолжают начисляться, как раньше.
User — пользователь SSO: id (= user_id от SSO), email, name (display_name),
avatar_url, locale, created_at
McpToken — персональный MCP-токен (3.10): user_id, token_hash (sha256,
уникален), label, created_at; plaintext показывается один раз
Task
id, title
description markdown — живёт в Document (owner_type = task)
task_type one_time | recurring — тип задачи: разовая / регулярная
status to_do | in_progress | done | cancelled | deferred
detail_state raw | approved — плоскость детализации (стека)
parent_task_id nullable — дерево подзадач
project_id nullable
tag_ids []
priority nullable (заполняется автодетализацией / вручную)
deadline nullable — дедлайн:
strict { date } — «до конкретной даты»
soft { period: week|month|year } — «в течение периода»
recurrence nullable — правило повторения (для task_type = recurring)
time_estimate nullable — прогноз времени (система)
budget nullable — ручной бюджет (деньги и/или время), опционален
estimated_cost nullable — оценка затрат по задаче (сравнивается с бюджетом)
actual_time nullable — фактические затраты (для обучения прогноза)
created_at, approved_at, done_at
Project
id, name
relevance_status active | paused — статус актуальности
is_archived bool — архив (3.8.1): проект и его задачи скрыты
из рабочих видов, восстановимы из «Архива»
pinned bool — булавка (3.8, 0.66): закреплённые проекты
держатся секцией «Закреплённые» вверху списка
priority
note markdown — живёт в Document (owner_type = project):
ссылки, контекст, картинки; ссылка на репозиторий —
в отдельном поле repository_url (0.67)
color nullable — цвет-метка проекта, HEX #rrggbb (3.8, 0.63);
палитра выбора — на фронте, бэкенд валидирует только формат
site_url nullable — сайт проекта, http(s)://... (3.8, 0.63);
иконка сайта тянется бэкенд-прокси с кэшем (3.11)
repository_url nullable — репозиторий проекта, http(s)://... (3.8, 0.67);
README оттуда тянется бэкенд-прокси с кэшем (3.11)
и показывается вторым описанием рядом с заметкой
Tag — справочник тегов
Document — markdown-текст с полиморфной привязкой к владельцу:
owner_type (task | project), owner_id, body (у задачи —
описание, у проекта — заметка); document всегда один на владельца
Attachment — файлы документов (изображения), document_id, mime.
URL скачивания файла неизменен: /api/attachments/{id}/file —
он зашит в сохранённый markdown-текст
XpEvent — начисление XP за закрытую задачу (идемпотентно по task_id):
amount, rarity (common | rare | epic), via_options
CoinEvent — журнал монет (заработок и траты; баланс = Σ amount):
task_id (nullable), level (nullable — бонус уровня),
source (task | level | daily | shop), amount (+/−), item_key
(ключ покупки для трат), created_at. В 0.58 источник баланса
для UI — `GET /api/xp` (поле coins); траты (source=shop)
возможны только из сада
GardenItem — элемент сцены сада: kind (plant | decoration),
ref_id (→ xp_events для растений; уникально в паре с kind),
item_key (вид растения / ключ декорации), x, y (ячейки сетки),
stage (0 росток, 1 куст, 2 цветение). С 0.58 новые записи
не создаются: ленивая ensure_garden вызывается только из
`GET /api/garden`, который UI не зовёт
AppSetting — per-user настройки (валюта, язык): PK (user_id, key), value
AppSettingGlobal — таблица-наследие однопользовательской версии
(бывшая app_settings); источник claim при первом логине,
позже удаляется
История завершённых задач (фактическое время) — источник для прогнозирования длительности.
┌─────────────┐ ┌────────────────────── VPS (docker-compose) ─────────────┐
│ Web SPA │────▶│ API (FastAPI) ──▶ PostgreSQL │
│ Vue 3 + │ │ ▲ │ │
│ gnexus- │ │ │ ├──▶ Сервис автодетализации ──▶ Ollama (модель │
│ ui-kit │ │ │ │ — LLM, модель из конфига) │
└─────────────┘ │ │ ├──▶ Хранилище вложений (файлы) │
▲ │ │ └──▶ MCP-сервер (для ИИ-агентов) │
└─ SSE ───────┤ шина событий (realtime) │
└─────────────────────────────────────────────────────────┘
▲ SSO auth.gnexus.space (клиент gnexus-gauth)
| Этап | Содержание |
|---|---|
| M0 — каркас | Репозиторий, скелет backend + frontend, SSO-интеграция (gnexus-gauth), деплой на VPS |
| M1 — MVP задач | CRUD задач, быстрый ввод, стек входящих, теги, проекты (базово), ручная детализация |
| M2 — детализация | Интеграция Ollama + модель из конфига, автопредзаполнение метаданных, подтверждение/редактирование, Markdown-редактор, вложения из буфера |
| M3 — структура | Подзадачи (parent_task_id), классический список с фильтрами, заметки проектов |
| M4 — умность | Прогнозирование времени, бюджет, выдача задачи |
| M5 — MCP | MCP-сервер, инструменты для агентов |
| M6 — мобильность | PWA (Android-приложение — позже, вне стартового скоупа) |
| M7 — упаковка | Docker (docker-compose: API, PostgreSQL, фронт, Ollama + модель) |
Порядок M4/M5 может меняться — MCP можно поднять раньше ради ИИ-сценария.
qwen3.5:2b-q4_K_M (проверена на M2); меняется конфигом (OLLAMA_MODEL) без правок кода. Ollama — внешний сервер, адрес — OLLAMA_BASE_URL.cancelled / deferred: определения зафиксированы (3.4), но поведение в интерфейсе почти одинаковое — скрыть из активных списков и оставить доступным для возврата. Уточнить при проектировании представлений, нужны ли оба статуса или их поведение сольётся.spawned_at), выход из done сбрасывает done_at. Повторные закрытия и обновления закрытой не дублируют награды и экземпляры — покрыто тестами.GET /api/garden; вопросы — что переделать в саду (сцена, маркет, прогрессия) — решать тогда, на накопленных данных.