- TypeScript 85.9%
- CSS 10.7%
- JavaScript 1.6%
- Dockerfile 0.9%
- HTML 0.5%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .githooks | ||
| apps | ||
| catalog | ||
| packages/shared | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| AGENTS.md | ||
| build.json | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| eslint.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.base.json | ||
Винка — список покупок
Self-hosted PWA «Винка» — совместный список покупок для семьи/соседей. Один Docker-контейнер раздаёт фронтенд (PWA) и бэкенд (REST + WebSocket + SQLite) на одном порту. Все списки доступны каждому, кто открыл главнюю страницу — авторизации и share-ссылок нет.
Запуск за 30 секунд
docker compose up -d --build
# открыть http://localhost:8080
Хост-порт задаётся APP_PORT в .env (APP_PORT=9090 → http://localhost:9090), лимит WS — MAX_WS_CLIENTS.
./catalog смонтирован read-only, поэтому новые группы товаров подхватываются без пересборки образа.
Данные — в ./data/lists.db (WAL). Приложение работает под UID 10001; entrypoint при старте
сам выставляет владельца ./data (ручной chown не нужен, каталог создаётся автоматически).
Бэкап: VACUUM INTO без остановки или остановка + копирование lists.db*.
Переменные окружения:
| Переменная | По умолчанию | Назначение |
|---|---|---|
PORT |
8080 |
HTTP/WS порт |
DATA_DIR |
/data (в контейнере) |
каталог БД |
MAX_WS_CLIENTS |
200 |
лимит WS-подключений |
CATALOG_DIR |
<репозиторий>/catalog |
каталог JSON-групп периодически покупаемых товаров |
HTTPS — на вашем обратном прокси (caddy/traefik/nginx). Пример Caddy:
lists.example.com {
reverse_proxy localhost:8080
}
PWA ставится на домашний экран (Chrome: «Установить приложение»).
Деплой на сервер (Proxmox + Dockhand)
-
Виртуальная машина Proxmox: Debian 12/13 (шаблон cloud-init), ~1 vCPU / 1 ГБ / 15 ГБ, мост
vmbr0. Docker на сам хост Proxmox не ставят. -
Docker Engine в ВМ:
curl -fsSL https://get.docker.com | sh, затемusermod -aG docker $USER. -
Dockhand (если ещё нет) — с «matching paths», чтобы относительные bind-mount'ы стеков (
./data) работали:mkdir -p /opt/dockhand docker run -d --name dockhand --restart unless-stopped \ -p 3000:3000 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/dockhand:/opt/dockhand \ -e DATA_DIR=/opt/dockhand \ fnsys/dockhand:latestОткрыть
http://<vm>:3000→ Settings → Authentication → создать администратора. -
Код приложения на сервер:
git clone(приватный репозиторий — тогда можно и автодеплой по push через Stacks → Git) илиrsync -a --exclude node_modules --exclude dist --exclude data --exclude '*.rar' ./ <user>@<vm>:/opt/stacks/vinka/. Каталогdata/создаётся автоматически, права контейнера (UID 10001) entrypoint выставит при старте. -
Stack в Dockhand: Stacks → New Compose Stack → вставить
docker-compose.yml(или указать путь к нему) → Deploy — образ соберётся на сервере (первая сборка дольше: нативная компиляция better-sqlite3). Проверка:GET http://<vm>:8080/api/health. -
Обновление:
git pull+ redeploy стека из Dockhand (илиdocker compose up -d --build). Схема SQLite мигрируется автоматически при старте. -
Бэкапы: снапшот/
vzdumpВМ + копияdata/lists.db*(в Dockhand есть Backups для bind-mount'ов с расписанием; либо cron:rsyncкаталогаdata/).
Модель доступа
- Авторизации нет: любой, кто открыл страницу приложения, видит и может менять все списки. Это осознанное решение для доверенного круга (дом/локальная сеть).
- Приложение не предназначено для публичного интернета без доступа по VPN/локальной сети.
- Все участники видят изменения в реальном времени (WebSocket, одно соединение на клиента, события всех списков).
- Настройки темы/тонирования хранятся локально в браузере (у каждого участника свои).
Архитектура
apps/web PWA: Vite + React + TypeScript (service worker, manifest)
apps/server Node 20+ / Fastify + @fastify/websocket + better-sqlite3 (WAL)
packages/shared общие типы моделей и WS-протокол (v: 1)
- Бэкенд раздаёт собранные статики PWA (
@fastify/static, SPA-fallback) + API/WS на одном порту. - Мутации идемпотентны: клиент генерирует UUID; повтор = upsert.
- Realtime: клиент держит одно WS-соединение и получает события всех списков
(
list.created/updated/removed,item.added/updated/removed/bought/returned); после восстановления соединения клиент делает полную синхронизацию. - Все списки хранятся на сервере и отдают всем; локальный кэш — для офлайн-отображения до синхронизации.
API
GET /api/health liveness
POST /api/lists создать (id/name/color, upsert)
GET /api/lists все списки (доступно каждому)
GET /api/lists/{id} полный снимок списка
PATCH /api/lists/{id} имя / цвет / порядок / группа каталога (catalogGroup, null — отвязать)
DELETE /api/lists/{id} удалить (можно удалить все списки)
POST /api/lists/{id}/items добавить позицию
PATCH /api/items/{id} text / urgent / comment
DELETE /api/items/{id} удалить позицию
POST /api/items/{id}/buy в историю (boughtAt ставит сервер)
POST /api/items/{id}/return возврат из истории (urgent=false, в конец)
GET /api/catalog группы каталога: id, file, name, count
GET /api/catalog/{id} полный список товаров группы
WS /ws события всех списков (одно соединение на клиента)
Каталог товаров
catalog/*.json — группы периодически покупаемых товаров (формат {"name": "...", "items": ["..."]},
id группы = имя файла без .json). В настройках списка (⋯ → «Каталог») можно привязать к списку
одну или несколько групп: видно файлы и количество записей, есть просмотр полного списка группы.
Разработка
npm install
# better-sqlite3 нужен собранный native-модуль: если npm не скачал prebuild —
# возьмите build/Release/better_sqlite3.node из релиза WiseLibs/better-sqlite3
# (v{version}-node-v{abi}-{platform}-{arch}.tar.gz) и положите в
# node_modules/better-sqlite3/build/Release/.
npm run build # shared → server → web
npm start # собранное приложение: API + PWA на http://localhost:8080
npm test # vitest: общие пакеты + API/WS (supertest)
npm run dev # сервер на :8080 (tsx watch)
npm run dev:web # vite dev-сервер на :5173 с прокси /api и /ws на :8080
Тесты Playwright (e2e приёмочных сценариев): npm run build -w @fcl/web,
затем npx playwright install chromium && npm test:e2e -w @fcl/web.
CI: npm run lint && npm test && npm run build.