Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Учёт рабочего времени сотрудников: таймер с описанием задачи и отчёт за период. Next.js, TypeScript, SQLite. Тестовое задание.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Демо: https://worktime-tracker-nu.vercel.app — данные вымышленные, менять их может любой посетитель. Каждую ночь демо сбрасывается к свежим вымышленным данным за прошлую неделю.
Веб-приложение для учёта рабочего времени сотрудников, сделанное как тестовое задание. В нём можно добавлять сотрудников, запускать и останавливать таймер с описанием задачи и смотреть записи и общее время каждого сотрудника за выбранный период. Данные хранятся в SQLite и не теряются при перезапуске.
Все имена, задачи и записи в приложении, тестах и на скриншотах вымышленные.
Подробности: план работы — docs/plan.md, журнал работы с ИИ — docs/ai-log.md.
Нужен Node.js 22 или новее.
npm install
npm run db:seed
npm run dev
Приложение откроется на http://localhost:3000.
npm run db:seed необязателен. Он добавляет в пустую базу четырёх вымышленных сотрудников и их записи за рабочие дни последней недели, чтобы отчёт не был пустым.data/worktime.db. npm run dev и npm start при каждом запуске применяют миграции, так что база создаётся сама, а после перезапуска всё на месте.Production-сборка:
npm run build
npm start
Без настроек приложение работает с локальным файлом. Чтобы подключить другую базу, скопируйте .env.example в .env.local.
| Переменная | По умолчанию | Для чего |
|---|---|---|
DATABASE_URL | пусто, то есть file:./data/worktime.db | файл SQLite или libsql://… для базы на Turso |
DATABASE_AUTH_TOKEN | пусто | токен доступа к Turso |
TURSO_DATABASE_URL, TURSO_AUTH_TOKEN | пусто | то же; их добавляет интеграция Turso Cloud на Vercel, используются, если DATABASE_URL пуст |
CRON_SECRET | пусто | секрет ежедневного сброса демо (см. «Развёртывание на Vercel»); без него маршрут сброса выключен |
Именно .env.local, а не .env.development: его читают и next dev, и next start, и скрипты db:migrate и db:seed.
| Команда | Что делает |
|---|---|
npm run dev | миграции и dev-сервер |
npm run build, npm start | production-сборка; npm start применяет миграции и запускает её |
npm run db:seed | вымышленные данные в пустую базу; npm run db:seed -- --reset сначала удаляет все данные |
npm test | unit- и интеграционные тесты (Vitest) |
npm run test:e2e | браузерные тесты (Playwright); нужен установленный Google Chrome |
npm run check | ESLint, проверка типов и тесты одной командой |
npm run format | форматирование (Prettier) |
TURSO_DATABASE_URL и TURSO_AUTH_TOKEN, а приложение читает их, если не задан DATABASE_URL.vercel.json: npm run db:migrate && TZ=Europe/Moscow npm run db:seed && npm run build. Миграции применяются при каждом развёртывании, а пустая база получает вымышленные данные с рабочим днём по московскому времени. Токен базы при этом не покидает Vercel.dub1 (Дублин), рядом с базой в регионе Turso aws-eu-west-1 (Ирландия).GET /api/cron/reset-demo, и все данные заменяются вымышленными за прошлую неделю. Так отчёт демо не опустеет со временем.
CRON_SECRET в заголовке Authorization: Bearer …; секрет сравнивается за постоянное время. Без CRON_SECRET маршрут не существует (404), с неверным секретом — 401.TZ там зарезервирована), поэтому сид раскладывает рабочий день по явному часовому поясу, а не по часам процесса.| Требование | Как сделано | Где проверяется |
|---|---|---|
| Добавление сотрудников | имя без пробелов по краям, от 1 до 100 символов, хотя бы одна буква или цифра | tests/integration/employees.test.ts, tests/api/handlers.test.ts, e2e |
| Старт и стоп таймера с описанием задачи | старт создаёт запись без времени окончания, стоп закрывает именно эту запись | tests/integration/timer.test.ts, tests/api/handlers.test.ts, e2e |
| Записи и общее время за период | период от местной полуночи до местной полуночи, по умолчанию последние 7 дней; записи обрезаются по его границам, идущий таймер считается до момента ответа | tests/unit/report.test.ts, tests/integration/report.test.ts, tests/unit/period.test.ts, e2e |
| Сохранение после перезапуска | файл SQLite; идущий таймер — строка в базе, а не состояние процесса | tests/integration/persistence.test.ts, ручная проверка с остановкой сервера |
| Тест повторного запуска таймера | все прочтения, см. ниже | tests/integration/timer-restart.test.ts, tests/integration/persistence.test.ts |
| Тест пустого отчёта | нули, а не null: и когда записей нет, и когда нет сотрудников | tests/unit/report.test.ts, tests/integration/report.test.ts, tests/api/handlers.test.ts |
| Простой интерфейс без авторизации | две страницы: «Сотрудники» и «Отчёт» | e2e на компьютере и на телефоне |
| Только вымышленные данные | выдуманные имена в демо-данных и тестах: фамилии значат «тест», «пример», «образец» и «проба» | src/server/db/seed.ts |
«Повторный запуск таймера». В английской версии задания сказано «restarting the timer», в русской — «повторный запуск таймера». Чтобы не гадать, тестами покрыты все прочтения:
Next.js 16 (App Router) и React 19, TypeScript, SQLite через libSQL и Drizzle ORM, zod 4, Tailwind CSS 4. Тесты — Vitest 5 и Playwright.
Интерфейс и API живут в одном приложении. SQLite не требует отдельного сервера, а тот же драйвер libSQL подключается к Turso, поэтому для демо подходит тот же код.
src/
├─ app/ страницы и API-маршруты Next.js
├─ components/ интерфейс: страница сотрудников, отчёт, два своих примитива
├─ lib/ код интерфейса без React: клиент API, периоды, форматирование
└─ server/
├─ db/ схема, подключение, вымышленные данные
├─ tracking/ правила учёта: сервис, отчёт, проверка ввода, ошибки
└─ http/ HTTP-слой: обработчики, статусы ошибок, защита запросов
drizzle/ SQL-миграции
scripts/ миграции и демо-данные из командной строки
tests/ unit, integration, api, smoke и e2e
docs/ план, журнал работы с ИИ, скриншоты
employees time_entries
────────────────────────── ──────────────────────────────────────────
id integer, ключ id integer, ключ
name text employee_id integer → employees.id
created_at integer, мс UTC description text
started_at integer, мс UTC
ended_at integer, мс UTC; NULL, пока идёт
started_at и ended_at, так что рассинхронизироваться нечему.one_running_timer_per_employee ON time_entries (employee_id) WHERE ended_at IS NULL. Проверку в коде два одновременных запроса (двойной клик, вторая вкладка) могли бы пройти оба, а индекс пропустит только один.CHECK (ended_at IS NULL OR ended_at >= started_at) и индекс (employee_id, started_at) для отчёта.| Запрос | Что делает | Ответ |
|---|---|---|
GET /api/employees | сотрудники по алфавиту; у каждого runningEntry — идущая запись или null | 200 |
POST /api/employees с {"name": "…"} | добавляет сотрудника | 201; 400 |
POST /api/employees/:id/timer/start с {"description": "…"} | запускает таймер | 201; 400, 404, 409 |
POST /api/time-entries/:id/stop | останавливает эту запись, если она ещё идёт | 200; 404, 409 |
GET /api/report?from=…&to=…&employeeId=… | записи и итоги за период [from, to); employeeId необязателен | 200; 400, 404 |
from и to — моменты в формате ISO 8601 со смещением: 2026-10-27T00:00:00+05:00 или 2026-10-26T19:00:00Z. Время без смещения отклоняется, потому что оно неоднозначно.Content-Type: application/json, иначе ответ 415. Запрос, который браузер пометил как пришедший с чужого сайта, получает 403. Подробнее — в разделе Безопасность.{"error": {"code": "TIMER_ALREADY_RUNNING", "message": "Таймер уже запущен"}}. Исключение — неподдерживаемый метод, например GET на маршрут, который принимает только POST: на него Next.js отвечает 405 без тела. Коды: VALIDATION_ERROR и INVALID_JSON (400), CROSS_SITE_REQUEST (403), EMPLOYEE_NOT_FOUND и ENTRY_NOT_FOUND (404), TIMER_ALREADY_RUNNING и TIMER_NOT_RUNNING (409), UNSUPPORTED_MEDIA_TYPE (415), INTERNAL_ERROR (500).Примеры для bash (Linux, macOS, WSL). Номер 1 подходит для пустой базы, после npm run db:seed возьмите номера из ответов.
# Добавить сотрудника → 201
curl -X POST http://localhost:3000/api/employees \
-H "Content-Type: application/json" \
-d '{"name": "Алия Тестова"}'
# Запустить его таймер → 201, запись с "endedAt": null
curl -X POST http://localhost:3000/api/employees/1/timer/start \
-H "Content-Type: application/json" \
-d '{"description": "Сверка накладных"}'
# Остановить эту запись → 200, теперь с endedAt
curl -X POST http://localhost:3000/api/time-entries/1/stop \
-H "Content-Type: application/json"
# Отчёт за 27 октября 2026 года по времени UTC+5
curl -G http://localhost:3000/api/report \
--data-urlencode "from=2026-10-27T00:00:00+05:00" \
--data-urlencode "to=2026-10-28T00:00:00+05:00"
--data-urlencode нужен из-за + в смещении: незакодированный + сервер прочитает как пробел и ответит 400.
SUM(). Записи нужно обрезать по границам периода, а SUM() на пустом наборе вернул бы NULL. Вся логика отчёта — три чистые функции в src/server/tracking/report.ts, их тесты не трогают базу.createApiHandlers тестируется обычными объектами Request на настоящем сервисе, а файлы маршрутов занимают по 5–9 строк.Record<ErrorCode, number>, поэтому новый код без статуса не пройдёт проверку типов. Неожиданная ошибка превращается в общий ответ 500: в тексте ошибки драйвера есть параметры запроса, а значит, и данные пользователя, поэтому он остаётся только в серверном логе.| Набор | Команда | Что проверяет |
|---|---|---|
| unit | npm test | чистые функции: отчёт, периоды и часовые пояса, форматирование, клиент API |
| integration | npm test | сервис на настоящей базе SQLite во временной папке, с управляемыми часами |
| api | npm test | HTTP-обработчики: статусы, коды ошибок, защита изменяющих запросов |
| smoke | npm test | клиент базы: создание файла, внешние ключи |
| e2e | npm run test:e2e | семь сценариев в Chrome, на компьютере и на телефоне (Pixel 7) |
npm test запускает 147 тестов в 21 файле.data/e2e.db, так что запущенный npm run dev им не мешает.Asia/Tashkent, Europe/Berlin). В CI стоит UTC, и там не видны две классические ошибки: new Date("2026-10-01") разбирается как полночь UTC, а не местная, а конец дня через «+24 часа» теряет час в день перевода часов.main и на каждый pull request запускает ESLint, проверку типов, тесты, Prettier, сборку и e2e. Если что-то упало, трассировки Playwright сохраняются как артефакт.| Инструмент | Модель | Для чего |
|---|---|---|
| Claude Code, десктоп-приложение | Claude Opus 5.5 | разбор задания, план, архитектура, код и тесты по TDD, отладка, документация |
| Отдельный агент Claude Code | Claude Opus 5.5 | независимое ревью кода, а перед подачей — оценка работы глазами проверяющего; агент не видел истории разработки и не мог менять репозиторий |
| Встроенный браузер Claude Code | — | ручные проверки интерфейса: замеры геометрии страницы, сетевой журнал, консоль |
| Claude in Chrome (расширение) | — | развёртывание в браузере владельца: проект Vercel, база Turso, проверка демо |
| Плагин security-guidance | — | автоматическое ревью безопасности коммитов |
Claude Code выбран потому, что работает прямо в репозитории как агент: сам запускает тесты, читает документацию установленных пакетов и проверяет интерфейс в браузере. Поэтому почти каждое утверждение ИИ можно было проверить, а не принимать на веру. Порядок работы задавали навыки (skills) Claude Code: TDD, систематическая отладка, план, дизайн интерфейса.
node_modules и с типами.Дословно, как они были отправлены; все запросы — в журнале.
https://jobster.falconai.app/apply/8cf49a7e89 На этом сайте есть тестовое задание. Вот думаю попробовать. Это именно то что я умею. Как будем делать тестовое задание. Напиши план
ИИ прочитал задание в русской и английской версиях, заметил расхождение «повторный запуск таймера» и «restarting the timer» и предложил покрыть тестами все прочтения.
Как сам думаешь на чем лучше всего сделать. Хочу показать себя с лучшей стороны
ИИ сравнил три стека по критериям задания и рекомендовал Next.js + SQLite. Решение человека: «Берём A: Next.js + TypeScript + SQLite, демо на Vercel + Turso. Пиши детальный план и начинай этап 0.»
Добавь защиту от межсайтовых запросов по TDD (Sec-Fetch-Site и Content-Type), запиши в журнал, потом начинай этап 4.
Ответ на автоматическое ревью безопасности. Красные тесты сначала подтвердили риск: межсайтовый POST и «простой» POST с text/plain создавали сотрудника.
Сделай стоп по entryId по TDD, защиту от DNS rebinding (белый список Host) опиши в README как ограничение, потом переходи к этапу 5.
Перед этим ИИ заметил при ручной проверке, что опоздавший стоп может остановить новый таймер, а второе автоматическое ревью безопасности указало на DNS rebinding. Человек решил: стоп исправить, DNS rebinding описать как ограничение.
Этап 5 посмотрел, всё ок. Находки 4 и 7 оставляем ограничениями в README. Начинай этап 6: полный README.
Решение по двум находкам независимого ревью.
Запрос, который ИИ написал сам, — начало задания для агента-ревьюера:
You are an independent code reviewer. Review a small web application for correctness bugs and security problems. You have no prior context on how it was built; form your own view from the code. Do NOT edit, create or delete any files in the repository, do not commit, and do not run git commands that change state.
Подробно, с причинами и стражами, — в журнале.
| № | Ошибка | Как нашли | Коммит |
|---|---|---|---|
| 1 | На Windows файл базы не удалялся после закрытия клиента | smoke-тесты, эксперимент со сборщиком мусора | 75039be |
| 2 | Лишний PRAGMA foreign_keys: ИИ перенёс на libSQL поведение обычного SQLite | тест прошёл сразу, хотя должен был упасть | 75039be |
| 3 | Ошибочный совет доверить сертификат, подменявший HTTPS при установке пакетов | проверка цепочки сертификатов; совет отозван | — |
| 4 | Флаг регулярного выражения, недоступный для цели компиляции ES2017 | проверка типов | 9d5cc8f |
| 5 | Изменяющие запросы принимались с чужого сайта и в виде text/plain | автоматическое ревью безопасности, красные тесты | 476dc95 |
| 6 | На телефоне строка с идущим таймером выходила за экран на 107 px | проверка в браузере, замер геометрии | 4f0f8c3 |
| 7 | Опоздавший стоп мог остановить новый таймер | ручная проверка с остановленным сервером | c77d301 |
| 8 | После горячей перезагрузки dev-сервер отвечал старым кодом сервиса | проверка в браузере, серверный лог | 6025d7b |
| 9 | Стоп отвечал 500, если часы сервера отстают от времени старта | независимое ревью, красный тест | f854dcc |
| 10 | Приложение можно было показать в невидимом фрейме на чужом сайте (кликджекинг) | независимое ревью, заголовки ответов сервера | ba7ee9d |
| 11 | Успешный ответ не в JSON ронял список сотрудников | независимое ревью, красный тест | 1096f5d |
| 12 | Имя из невидимого символа создавало сотрудника; сообщения об ошибках по-английски | независимое ревью, красные тесты | 75dae13 |
| 13 | Ответы на перезагрузку списка могли прийти не по порядку | независимое ревью, чтение кода | 75e90da |
| 14 | В отчёте не было видно даты окончания записи, перешедшей через полночь |
Когда ревью нашло ошибки 9–17, все 108 тестов были зелёными: тесты проверяли то, что ИИ предусмотрел сам. Больше половины ошибок нашли не тесты, а проверка в браузере и ревью свежим агентом. Вторая проверка перед подачей, с чистого клона и глазами проверяющего, нашла ещё пять (18–22), и снова при зелёных тестах: уязвимость видна только при установке, а устаревание демо — только со временем.
Авторизации и ролей нет: по условию задания они не нужны. Поэтому любой, у кого есть доступ к приложению, видит всех сотрудников и может управлять любым таймером. В этих рамках сделано следующее.
Входные данные проверяются на сервере (zod): имена, описания задач, даты и идентификаторы.
Ошибки без подробностей. Неожиданная ошибка возвращается как общий ответ 500. Её текст, где могут быть SQL-запрос и данные пользователя, попадает только в серверный лог.
Защита от межсайтовых запросов. Изменяющие запросы принимаются только как JSON и только со страниц самого приложения:
Sec-Fetch-Site: cross-site или same-site → 403;Так страница чужого сайта не сможет через браузер посетителя записать данные в приложение, запущенное на localhost или во внутренней сети. Клиенты вне браузера, например curl, не присылают Sec-Fetch-Site и проходят проверку.
Запрет встраивания в чужие фреймы. Заголовки X-Frame-Options: DENY и Content-Security-Policy: frame-ancestors 'none' не дают показать приложение в невидимом фрейме на чужой странице и обманом заставить нажать «Остановить» (кликджекинг).
Таймер останавливается по идентификатору записи. Опоздавший или повторный запрос не остановит таймер, запущенный после него.
Зависимости проверяет CI. Шаг npm audit --audit-level=high падает на уязвимостях уровня high и critical во всём, что ставит npm ci. Сейчас остаются 4 умеренных предупреждения в drizzle-kit: они касаются dev-сервера esbuild, который проект не запускает, а исправления нет, кроме отката drizzle-kit на старую версию.
127.0.0.1. Для браузера его страница становится «тем же сайтом», и проверки выше её пропускают.Host. Запрос принимается, только если Host — это localhost, 127.0.0.1 или домен развёртывания. На Vercel домен берётся из VERCEL_URL и VERCEL_PROJECT_PRODUCTION_URL, на своём сервере — из переменной окружения, например ALLOWED_HOSTS.npm run db:seed -- --reset с адресом и токеном базы Turso в .env.local.| независимое ревью, красные тесты |
e787f15 |
| 15 | Отчёт устаревал и никак об этом не сообщал | независимое ревью, красные e2e-тесты | 7701c56 |
| 16 | После «Начать» и «Остановить» терялся фокус клавиатуры | независимое ревью, красный e2e-тест | e01a549 |
| 17 | Тест в обход сервиса принимал любую ошибку, а не только нарушение индекса | независимое ревью | 89a1e4a |
| 18 | Next.js был закреплён на версии с критическими уязвимостями, хотя исправленная уже вышла | npm install в чистом клоне, npm audit | 5017515 |
| 19 | npm start падал без dev-зависимостей: миграциям нужны tsx и @next/env | оценка проверяющего, установка без dev-зависимостей | 097926b |
| 20 | Новый шаг аудита в CI не проверял инструменты разработки | автоматическое ревью безопасности | dd89e6e |
| 21 | Короткие записи читались как «0 мин» + «0 мин» = «1 мин» | проверка глазами проверяющего | 8f16c18 |
| 22 | Демо-данные устаревали: со 2 ноября отчёт демо по умолчанию был бы пуст | оценка проверяющего | da79dfb |