init: scaffold проекта — schema, docker-compose, nginx, README, CI
Deploy / deploy (push) Failing after 20s
Deploy / deploy (push) Failing after 20s
- db/schema.sql: полная PostgreSQL 16 схема (12 таблиц + view works_extended) - docker-compose.yml: db + backend + frontend + nginx - nginx/nginx.conf: HTTPS proxy, WebSocket, static uploads - .env.example: все переменные задокументированы - .gitea/workflows/deploy.yml: CI деплой-заготовка - README.md: граф задач флота агентов Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://postgres:PASSWORD@db:5432/nika_gallery
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
JWT_SECRET=change-me-jwt-secret
|
||||||
|
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
|
||||||
|
# Email
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=noreply@nika-gallery.ru
|
||||||
|
SMTP_PASS=smtp-password
|
||||||
|
OWNER_EMAIL=nika@example.com
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=bot:token
|
||||||
|
TELEGRAM_CHANNEL_ID=@nika_channel
|
||||||
|
OWNER_TELEGRAM_ID=123456789
|
||||||
|
|
||||||
|
# App
|
||||||
|
SITE_URL=https://nika-gallery.ru
|
||||||
|
PORT=4000
|
||||||
|
NODE_ENV=production
|
||||||
|
UPLOAD_DIR=/app/uploads
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
NEXT_PUBLIC_API_URL=https://nika-gallery.ru/api
|
||||||
|
NEXT_PUBLIC_WS_URL=wss://nika-gallery.ru/ws
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Gitea Actions — деплой при push в main
|
||||||
|
# Требует: runner на prod-сервере + секреты DEPLOY_HOST, DEPLOY_KEY
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Deploy via SSH
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.DEPLOY_KEY }}" > /tmp/deploy_key
|
||||||
|
chmod 600 /tmp/deploy_key
|
||||||
|
ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \
|
||||||
|
root@${{ secrets.DEPLOY_HOST }} \
|
||||||
|
"cd /opt/nika-gallery && git pull && docker compose build && docker compose up -d"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
uploads/
|
||||||
|
dist/
|
||||||
|
.next/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Ника — Галерея & Аукцион
|
||||||
|
|
||||||
|
Сайт дизайнера Ники: галерея работ, аукцион, подписки, Telegram-интеграция.
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
- **Backend:** Node.js/Express (ESM), PostgreSQL 16
|
||||||
|
- **Frontend:** Next.js 14 App Router, TypeScript, Tailwind CSS
|
||||||
|
- **Real-time:** WebSocket (ws)
|
||||||
|
- **Telegram:** grammy
|
||||||
|
- **Deploy:** Docker Compose + Nginx + Let's Encrypt
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
```
|
||||||
|
nika-gallery/
|
||||||
|
├── backend/ # Express API (порт 4000)
|
||||||
|
├── frontend/ # Next.js (порт 3000)
|
||||||
|
├── db/ # schema.sql + seed.sql
|
||||||
|
├── nginx/ # nginx.conf
|
||||||
|
├── scripts/ # import-channel.mjs и утилиты
|
||||||
|
└── docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Задачи флота агентов
|
||||||
|
|
||||||
|
| # | Задача | Статус | Блокирует |
|
||||||
|
|---|--------|--------|-----------|
|
||||||
|
| 1 | БД-схема + API-контракт | ✅ schema.sql готова | — |
|
||||||
|
| 2 | Backend scaffold + Auth | pending | 1 |
|
||||||
|
| 3 | Frontend scaffold | pending | 1 |
|
||||||
|
| 4 | Галерея: CRUD + медиа | pending | 2,3 |
|
||||||
|
| 5 | Аукцион: primary/secondary + WS | pending | 4 |
|
||||||
|
| 6 | Подписки + рассылка | pending | 2,3 |
|
||||||
|
| 7 | Социальный слой | pending | 4 |
|
||||||
|
| 8 | Уведомления владельцу | pending | 5,7 |
|
||||||
|
| 9 | Telegram-бот публикации | pending | 4 |
|
||||||
|
| 10 | Импорт из TG-канала | pending | 9 |
|
||||||
|
| 11 | Docker + деплой + SSL | pending | 2,3 |
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# отредактировать .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
Смотри `.env.example`
|
||||||
|
|
||||||
|
## Связь с оператором
|
||||||
|
- Telegram: @Ya26_bot (chat_id 1113994683)
|
||||||
|
- Агент-ноутбук: Claude Code на ноутбуке Германа
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
-- nika-gallery: полная схема PostgreSQL 16
|
||||||
|
-- Создано: ops-agent 2026-08-05
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- Пользователи
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'buyer' CHECK (role IN ('owner','admin','buyer','subscriber')),
|
||||||
|
telegram_id BIGINT,
|
||||||
|
email_verified_at TIMESTAMPTZ,
|
||||||
|
refresh_token TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Категории работ
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Работы
|
||||||
|
CREATE TABLE works (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
medium TEXT, -- техника: масло, акварель...
|
||||||
|
dimensions TEXT, -- размер: 60x80 см
|
||||||
|
year INT,
|
||||||
|
price NUMERIC(12,2),
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft'
|
||||||
|
CHECK (status IN ('draft','published','sold')),
|
||||||
|
auction_mode TEXT NOT NULL DEFAULT 'none'
|
||||||
|
CHECK (auction_mode IN ('none','primary','secondary')),
|
||||||
|
telegram_message_id BIGINT,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Категории работ (M2M)
|
||||||
|
CREATE TABLE work_categories (
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
category_id INT REFERENCES categories(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (work_id, category_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Медиа
|
||||||
|
CREATE TABLE media (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
thumb_url TEXT,
|
||||||
|
type TEXT NOT NULL DEFAULT 'image' CHECK (type IN ('image','video')),
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Аукционы
|
||||||
|
CREATE TABLE auctions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('primary','secondary')),
|
||||||
|
start_price NUMERIC(12,2) NOT NULL,
|
||||||
|
current_price NUMERIC(12,2) NOT NULL,
|
||||||
|
reserve_price NUMERIC(12,2),
|
||||||
|
min_step NUMERIC(12,2) NOT NULL DEFAULT 500,
|
||||||
|
start_at TIMESTAMPTZ NOT NULL,
|
||||||
|
end_at TIMESTAMPTZ NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending','active','ended','sold','cancelled')),
|
||||||
|
winner_id UUID REFERENCES users(id),
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Ставки
|
||||||
|
CREATE TABLE bids (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
auction_id UUID REFERENCES auctions(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
amount NUMERIC(12,2) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Подписки
|
||||||
|
CREATE TABLE subscriptions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT,
|
||||||
|
confirm_token TEXT NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
unsub_token TEXT NOT NULL DEFAULT gen_random_uuid()::text,
|
||||||
|
confirmed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Категории подписки (опционально)
|
||||||
|
CREATE TABLE subscription_categories (
|
||||||
|
subscription_id UUID REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||||||
|
category_id INT REFERENCES categories(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (subscription_id, category_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- История рассылок
|
||||||
|
CREATE TABLE newsletter_sends (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
subject TEXT NOT NULL,
|
||||||
|
body_html TEXT NOT NULL,
|
||||||
|
recipient_count INT NOT NULL DEFAULT 0,
|
||||||
|
sent_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Комментарии
|
||||||
|
CREATE TABLE comments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending','approved','rejected')),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Лайки
|
||||||
|
CREATE TABLE likes (
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (work_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Отзывы
|
||||||
|
CREATE TABLE reviews (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||||
|
body TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (work_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Уведомления владельца
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
owner_id UUID REFERENCES users(id),
|
||||||
|
type TEXT NOT NULL
|
||||||
|
CHECK (type IN ('comment','like','review','bid','sale','new_bid_lost')),
|
||||||
|
entity_id UUID, -- id комментария / лайка / ставки / аукциона
|
||||||
|
preview_text TEXT, -- «Иван: "Красиво!" (60 символов)»
|
||||||
|
admin_url TEXT, -- /admin/comments/uuid
|
||||||
|
read_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Telegram-синхронизация
|
||||||
|
CREATE TABLE telegram_sync (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
work_id UUID REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
message_id BIGINT NOT NULL,
|
||||||
|
direction TEXT NOT NULL DEFAULT 'push' CHECK (direction IN ('push','pull')),
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы
|
||||||
|
CREATE INDEX idx_works_status ON works(status);
|
||||||
|
CREATE INDEX idx_works_slug ON works(slug);
|
||||||
|
CREATE INDEX idx_auctions_status ON auctions(status);
|
||||||
|
CREATE INDEX idx_auctions_end_at ON auctions(end_at);
|
||||||
|
CREATE INDEX idx_bids_auction ON bids(auction_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_comments_work ON comments(work_id, status);
|
||||||
|
CREATE INDEX idx_notifications ON notifications(owner_id, read_at, created_at DESC);
|
||||||
|
|
||||||
|
-- View: работы с агрегатами
|
||||||
|
CREATE VIEW works_extended AS
|
||||||
|
SELECT
|
||||||
|
w.*,
|
||||||
|
COUNT(DISTINCT l.user_id) AS likes_count,
|
||||||
|
COUNT(DISTINCT c.id)
|
||||||
|
FILTER (WHERE c.status='approved') AS comments_count,
|
||||||
|
AVG(r.rating) AS avg_rating,
|
||||||
|
COUNT(DISTINCT r.id) AS reviews_count,
|
||||||
|
m.url AS cover_url,
|
||||||
|
m.thumb_url AS cover_thumb
|
||||||
|
FROM works w
|
||||||
|
LEFT JOIN likes l ON l.work_id = w.id
|
||||||
|
LEFT JOIN comments c ON c.work_id = w.id
|
||||||
|
LEFT JOIN reviews r ON r.work_id = w.id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT url, thumb_url FROM media
|
||||||
|
WHERE work_id = w.id ORDER BY sort_order LIMIT 1
|
||||||
|
) m ON true
|
||||||
|
GROUP BY w.id, m.url, m.thumb_url;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Seed: тестовые данные
|
||||||
|
-- Категории
|
||||||
|
INSERT INTO categories (name, slug, sort_order) VALUES
|
||||||
|
('Живопись', 'painting', 1),
|
||||||
|
('Иллюстрация', 'illustration', 2),
|
||||||
|
('Графика', 'graphics', 3),
|
||||||
|
('Цифровое искусство', 'digital', 4),
|
||||||
|
('Текстиль', 'textile', 5);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Секреты только из .env — не хардкодить здесь.
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: nika_gallery
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
- ./db/schema.sql:/docker-entrypoint-initdb.d/01_schema.sql:ro
|
||||||
|
- ./db/seed.sql:/docker-entrypoint-initdb.d/02_seed.sql:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:4000:4000"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/certs:/etc/nginx/certs:ro
|
||||||
|
- ./uploads:/var/www/uploads:ro
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
events { worker_connections 1024; }
|
||||||
|
|
||||||
|
http {
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name nika-gallery.ru www.nika-gallery.ru;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name nika-gallery.ru www.nika-gallery.ru;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||||
|
|
||||||
|
# Медиа-файлы — отдаёт Nginx напрямую
|
||||||
|
location /uploads/ {
|
||||||
|
alias /var/www/uploads/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# API + WebSocket
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:4000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://backend:4000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next.js SPA
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user