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 |