Compare commits

2 Commits
main ... dev

Author SHA1 Message Date
a30fe60414 Add local development environment with switchable backend
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
Set up local dev workflow: SSH tunnel to dev DB, Next.js API proxy
via rewrites, Docker or native backend on port 5000. Update and
restructure project documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 01:02:56 +02:00
2dadc5362d Add PostHog analytics with heatmap support
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:49:52 +02:00
22 changed files with 738 additions and 364 deletions

4
.gitignore vendored
View File

@@ -6,6 +6,7 @@ frontend/out/
# Backend # Backend
backend/src/*/bin/ backend/src/*/bin/
backend/src/*/obj/ backend/src/*/obj/
backend/.idea/
# Environment # Environment
.env .env
@@ -20,6 +21,9 @@ backend/src/*/obj/
*.swp *.swp
*.swo *.swo
# Local development
backend/src/*/appsettings.Local.json
# OS # OS
Thumbs.db Thumbs.db
.DS_Store .DS_Store

View File

@@ -28,7 +28,13 @@ GBSite/
│ └── Dockerfile # Multi-stage: dotnet sdk → aspnet runtime │ └── Dockerfile # Multi-stage: dotnet sdk → aspnet runtime
├── deploy/ ├── deploy/
│ ├── docker-compose.prod.yml │ ├── docker-compose.prod.yml
── docker-compose.dev.yml ── docker-compose.dev.yml
│ └── docker-compose.local.yml # Local backend (Docker mode)
├── scripts/
│ ├── local-tunnel.ps1 # SSH tunnel to dev DB
│ ├── local-backend-docker.ps1 # Start Docker backend
│ ├── local-backend-dotnet.ps1 # Start native backend
│ └── local-frontend.ps1 # Start frontend dev server
├── .woodpecker/ ├── .woodpecker/
│ └── deploy.yml # CI/CD pipeline │ └── deploy.yml # CI/CD pipeline
└── CLAUDE.md # This file └── CLAUDE.md # This file
@@ -40,8 +46,9 @@ GBSite/
|-----|-------------|---------|-----| |-----|-------------|---------|-----|
| **Prod** | https://new.goodbrick.com.ua | https://new.goodbrick.com.ua/api/* | prod_db | | **Prod** | https://new.goodbrick.com.ua | https://new.goodbrick.com.ua/api/* | prod_db |
| **Dev** | https://dev.goodbrick.com.ua | https://dev.goodbrick.com.ua/api/* | dev_db | | **Dev** | https://dev.goodbrick.com.ua | https://dev.goodbrick.com.ua/api/* | dev_db |
| **Local** | http://localhost:3000 | http://localhost:5000/api/* | dev_db (via SSH tunnel) |
## Docker Containers ## Docker Containers (Server)
| Container | Internal Port | Host Port | Network | | Container | Internal Port | Host Port | Network |
|-----------|--------------|-----------|---------| |-----------|--------------|-----------|---------|
@@ -58,7 +65,48 @@ Path-based routing — один домен для фронта и API:
Backend получает запросы с prefix `/api/` (Caddy использует `handle`, не `handle_path`). Backend получает запросы с prefix `/api/` (Caddy использует `handle`, не `handle_path`).
## Development ## Local Development
### Quick Start (3 терминала)
**Terminal 1 — SSH Tunnel (всегда нужен):**
```powershell
.\scripts\local-tunnel.ps1
```
**Terminal 2 — Backend (выбрать один):**
```powershell
# Работа с фронтом — бэкенд в Docker:
.\scripts\local-backend-docker.ps1
# Работа с бэком — dotnet run с hot-reload:
.\scripts\local-backend-dotnet.ps1
```
**Terminal 3 — Frontend:**
```powershell
.\scripts\local-frontend.ps1
```
### Как это работает
- **SSH Tunnel:** `localhost:5433` → сервер `31.131.18.254:5432` (dev_db)
- **Backend:** слушает на `localhost:5000`, подключается к БД через туннель
- **Frontend:** `npm run dev` на `:3000`, Next.js rewrites проксируют `/api/*``localhost:5000`
- **Переключение режимов:** Ctrl+C в терминале с бэком, запустить другой скрипт. Фронт перезапускать не нужно.
### Конфигурация
- `backend/src/GBSite.Api/appsettings.Local.json` — connection string к dev_db (gitignored)
- `frontend/.env.local``LOCAL_API_URL` и `INTERNAL_API_URL` для локального проксирования
- `deploy/docker-compose.local.yml` — Docker backend с `host.docker.internal:5433`
### URLs
- Frontend: http://localhost:3000
- Backend API: http://localhost:5000/api/health
- DB: localhost:5433 (через SSH tunnel к dev_db)
## Development (manual)
### Frontend ### Frontend
```bash ```bash
@@ -91,7 +139,7 @@ cd backend && dotnet build
- `src/components/` — React components (create when needed) - `src/components/` — React components (create when needed)
- shadcn/ui components via `npx shadcn@latest add <component>` - shadcn/ui components via `npx shadcn@latest add <component>`
- Server-side calls to backend use `INTERNAL_API_URL` env var - Server-side calls to backend use `INTERNAL_API_URL` env var
- Client-side calls use relative paths (`/api/...`) — Caddy proxies them - Client-side calls use relative paths (`/api/...`) — Caddy proxies them on server, Next.js rewrites locally
## Git & CI/CD ## Git & CI/CD
@@ -118,6 +166,7 @@ Host=postgres;Port=5432;Database={db};Username={user};Password={pass}
``` ```
Configured via env var `ConnectionStrings__Default` in docker-compose files. Configured via env var `ConnectionStrings__Default` in docker-compose files.
Locally via `appsettings.Local.json` with SSH tunnel (`localhost:5433`).
## Key Rules ## Key Rules
@@ -125,3 +174,4 @@ Configured via env var `ConnectionStrings__Default` in docker-compose files.
- Frontend standalone output (`output: "standalone"` in next.config.ts) — required for Docker - Frontend standalone output (`output: "standalone"` in next.config.ts) — required for Docker
- Backend listens on port 5000 (`ASPNETCORE_URLS=http://+:5000`) - Backend listens on port 5000 (`ASPNETCORE_URLS=http://+:5000`)
- All containers must be in `app-network` (external Docker network) - All containers must be in `app-network` (external Docker network)
- `appsettings.Local.json` is gitignored — local dev overrides only

129
README.md
View File

@@ -1,94 +1,59 @@
# GoodBrick Project # GoodBrick Project
Корпоративная инфраструктура для компании GoodBrick на базе self-hosted сервисов. Сайт производителя фасадной плитки GoodBrick — каталог продукции, корпоративная информация.
## 🚀 Deployed Services ## Services
| Service | URL | Status | Purpose | | Service | URL | Purpose |
|---------|-----|--------|---------| |---------|-----|---------|
| **Gitea** | https://git.goodbrick.com.ua | ✅ Running | Git сервер | | **Production** | https://new.goodbrick.com.ua | Продакшн приложение |
| **Uptime Kuma** | https://status.goodbrick.com.ua | ✅ Running | Мониторинг | | **Development** | https://dev.goodbrick.com.ua | Dev окружение |
| **Woodpecker CI** | https://ci.goodbrick.com.ua | ✅ Running | CI/CD | | **Gitea** | https://git.goodbrick.com.ua | Git сервер |
| **Production App** | https://new.goodbrick.com.ua | 🔨 TODO | Продакшн приложение | | **Woodpecker CI** | https://ci.goodbrick.com.ua | CI/CD |
| **Development App** | https://dev.goodbrick.com.ua | 🔨 TODO | Dev окружение | | **Uptime Kuma** | https://status.goodbrick.com.ua | Мониторинг |
## 📚 Documentation ## Stack
- **[SERVER.md](./SERVER.md)** - Полная документация по серверу, сервисам, Docker, базам данных и troubleshooting - **Frontend:** Next.js 16 + shadcn/ui + Tailwind CSS 4
- **Backend:** .NET 9 Web API + Npgsql
- **Database:** PostgreSQL 16
- **Proxy:** Caddy 2 (auto-SSL)
- **CI/CD:** Woodpecker CI
## 🖥️ Server Info ## Local Development
See [CLAUDE.md](./CLAUDE.md#local-development) for full local dev setup.
```powershell
# Terminal 1: SSH tunnel to dev database
.\scripts\local-tunnel.ps1
# Terminal 2: Backend (choose one)
.\scripts\local-backend-dotnet.ps1 # native, with hot-reload
.\scripts\local-backend-docker.ps1 # Docker container
# Terminal 3: Frontend
.\scripts\local-frontend.ps1
```
## Documentation
- **[CLAUDE.md](./CLAUDE.md)** — Project conventions, structure, local dev setup
- **[SERVER.md](./SERVER.md)** — Server infrastructure, services, troubleshooting
## Git Workflow
- `main` branch → auto-deploy to production
- `dev` branch → auto-deploy to dev environment
- Commit style: conventional, English
## Server
- **Host:** 31.131.18.254 - **Host:** 31.131.18.254
- **SSH:** `ssh deploy@31.131.18.254` (key-based auth) - **SSH:** `ssh deploy@31.131.18.254` (key-based)
- **OS:** Linux
- **Docker Network:** `app-network` - **Docker Network:** `app-network`
## 🗄️ Databases ## Next Steps
PostgreSQL 16 с отдельными базами для каждого сервиса: - [ ] Настроить email для Gitea
- `gitea_db` - Gitea - [ ] Настроить автобэкапы баз данных
- `prod_db` - Production app
- `dev_db` - Development app
Все credentials в `/srv/postgres/CREDENTIALS.txt` на сервере.
## 🔧 Stack
- **Reverse Proxy:** Caddy 2 (автоматический SSL)
- **Database:** PostgreSQL 16
- **Git:** Gitea
- **CI/CD:** Woodpecker CI
- **Monitoring:** Uptime Kuma
- **Container:** Docker + Docker Compose
## 📝 Quick Commands
### SSH в сервер
```bash
ssh deploy@31.131.18.254
```
### Просмотр всех контейнеров
```bash
ssh deploy@31.131.18.254 'docker ps'
```
### Просмотр логов
```bash
ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50'
```
### Рестарт сервиса
```bash
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart'
```
## 🏗️ Project Structure
```
C:\Work\goodbrick\GBSite\
├── README.md ← Этот файл
├── SERVER.md ← Полная документация сервера
└── [project files] ← Файлы проектов
```
## 🔐 Security
- SSH: Только key-based аутентификация
- PostgreSQL: Изолированные пользователи для каждого приложения
- Docker: Все сервисы в изолированной сети `app-network`
- SSL: Автоматические сертификаты через Let's Encrypt
## 🎯 Next Steps
1. Deploy production app на `new.goodbrick.com.ua`
2. Deploy development app на `dev.goodbrick.com.ua`
3. Настроить CI/CD пайплайны в Woodpecker
4. Настроить email для Gitea
5. Настроить автобэкапы баз данных
---
**Документация актуальна на:** 2026-02-09
**Разработчик:** Ivan
**AI Assistant:** Claude Sonnet 4.5

323
SERVER.md
View File

@@ -3,7 +3,7 @@
## SSH Access ## SSH Access
- **Host:** 31.131.18.254 - **Host:** 31.131.18.254
- **User:** deploy - **User:** deploy
- **Auth:** SSH key (already configured, no password needed) - **Auth:** SSH key (no password)
- **Command:** `ssh deploy@31.131.18.254` - **Command:** `ssh deploy@31.131.18.254`
## Server Structure ## Server Structure
@@ -12,7 +12,9 @@
``` ```
/srv/ /srv/
├── apps/ # Empty, ready for applications ├── apps/
│ ├── gb-site/ # Production app (main branch)
│ └── gb-site-dev/ # Development app (dev branch, git worktree)
├── gitea/ # Git server ├── gitea/ # Git server
├── postgres/ # PostgreSQL database ├── postgres/ # PostgreSQL database
├── proxy/ # Caddy reverse proxy ├── proxy/ # Caddy reverse proxy
@@ -27,12 +29,13 @@
## Live Services ## Live Services
### Production URLs | Service | URL |
- **Gitea:** https://git.goodbrick.com.ua |---------|-----|
- **Uptime Kuma:** https://status.goodbrick.com.ua | **Production App** | https://new.goodbrick.com.ua |
- **Woodpecker CI:** https://ci.goodbrick.com.ua ✅ Working! | **Development App** | https://dev.goodbrick.com.ua |
- **Production App:** https://new.goodbrick.com.ua (TODO) | **Gitea** | https://git.goodbrick.com.ua |
- **Development App:** https://dev.goodbrick.com.ua (TODO) | **Woodpecker CI** | https://ci.goodbrick.com.ua |
| **Uptime Kuma** | https://status.goodbrick.com.ua |
All domains point to `31.131.18.254` via A records. All domains point to `31.131.18.254` via A records.
@@ -49,6 +52,7 @@ All domains point to `31.131.18.254` via A records.
- **SSL:** Automatic via Let's Encrypt - **SSL:** Automatic via Let's Encrypt
#### Caddyfile Routes #### Caddyfile Routes
```caddyfile ```caddyfile
git.goodbrick.com.ua { git.goodbrick.com.ua {
reverse_proxy gitea:3000 reverse_proxy gitea:3000
@@ -65,17 +69,25 @@ ci.goodbrick.com.ua {
} }
new.goodbrick.com.ua { new.goodbrick.com.ua {
reverse_proxy new:3000 handle /api/* {
reverse_proxy gb-prod-backend:5000
}
handle {
reverse_proxy gb-prod-frontend:3000
}
} }
dev.goodbrick.com.ua { dev.goodbrick.com.ua {
reverse_proxy dev:3000 handle /api/* {
reverse_proxy gb-dev-backend:5000
}
handle {
reverse_proxy gb-dev-frontend:3000
}
} }
``` ```
**Important Notes:** **Important:** `handle` (not `handle_path`) preserves the `/api/` prefix in the request.
- Use container names (not `127.0.0.1` or `localhost`)
- `flush_interval -1` required for Woodpecker's Server-Sent Events
**Restart:** **Restart:**
```bash ```bash
@@ -88,7 +100,7 @@ ssh deploy@31.131.18.254 'cd /srv/proxy && docker compose restart'
- **Location:** `/srv/postgres` - **Location:** `/srv/postgres`
- **Container:** `postgres` - **Container:** `postgres`
- **Image:** `postgres:16` - **Image:** `postgres:16`
- **Port:** 127.0.0.1:5432 (localhost only) - **Port:** 127.0.0.1:5432 (localhost only, not exposed to internet)
- **Data:** `/srv/postgres/data` - **Data:** `/srv/postgres/data`
- **Credentials:** `/srv/postgres/CREDENTIALS.txt` (chmod 600) - **Credentials:** `/srv/postgres/CREDENTIALS.txt` (chmod 600)
@@ -102,34 +114,13 @@ Database: app
#### Application Databases #### Application Databases
**Gitea:** | App | User | Password | Database |
``` |-----|------|----------|----------|
User: gitea_user | Gitea | gitea_user | gitea_pass_zYWT5JWu3iAbbW7m | gitea_db |
Password: gitea_pass_zYWT5JWu3iAbbW7m | Production | prod_user | prod_pass_kL9mN2pQ7xR8sT4v | prod_db |
Database: gitea_db | Development | dev_user | dev_pass_vB6nM3qP8yW2rT9k | dev_db |
Connection String: postgres://gitea_user:gitea_pass_zYWT5JWu3iAbbW7m@postgres:5432/gitea_db
```
**Production App:** Each app has isolated DB and user. Users cannot access other databases.
```
User: prod_user
Password: prod_pass_kL9mN2pQ7xR8sT4v
Database: prod_db
Connection String: postgres://prod_user:prod_pass_kL9mN2pQ7xR8sT4v@postgres:5432/prod_db
```
**Development App:**
```
User: dev_user
Password: dev_pass_vB6nM3qP8yW2rT9k
Database: dev_db
Connection String: postgres://dev_user:dev_pass_vB6nM3qP8yW2rT9k@postgres:5432/dev_db
```
**Important:**
- Each app has its own isolated database and user
- Users cannot access other databases
- PostgreSQL only accessible via Docker network
**Restart:** **Restart:**
```bash ```bash
@@ -143,34 +134,13 @@ ssh deploy@31.131.18.254 'cd /srv/postgres && docker compose restart'
- **Container:** `gitea` - **Container:** `gitea`
- **Image:** `gitea/gitea:latest` - **Image:** `gitea/gitea:latest`
- **URL:** https://git.goodbrick.com.ua - **URL:** https://git.goodbrick.com.ua
- **Admin:** admin (set during installation) - **Ports:** 127.0.0.1:3002:3000 (HTTP), 127.0.0.1:2222:22 (SSH)
- **Ports:** - **Database:** gitea_db
- 127.0.0.1:3002:3000 (HTTP)
- 127.0.0.1:2222:22 (SSH)
- **Database:** gitea_db (PostgreSQL)
- **Data:** `/srv/gitea/data`
#### OAuth Application for Woodpecker #### OAuth Application for Woodpecker
- **Client ID:** 157422cf-7391-4fb0-8f2d-27083676dda6 - **Client ID:** 157422cf-7391-4fb0-8f2d-27083676dda6
- **Redirect URI:** https://ci.goodbrick.com.ua/authorize - **Redirect URI:** https://ci.goodbrick.com.ua/authorize
**Config:**
```yaml
environment:
- GITEA__server__DOMAIN=git.goodbrick.com.ua
- GITEA__server__ROOT_URL=https://git.goodbrick.com.ua/
- GITEA__webhook__ALLOWED_HOST_LIST=* # Allows webhooks to Woodpecker
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea_db
- GITEA__database__USER=gitea_user
```
**Restart:**
```bash
ssh deploy@31.131.18.254 'cd /srv/gitea && docker compose restart'
```
--- ---
### 4. Uptime Kuma (Monitoring) ### 4. Uptime Kuma (Monitoring)
@@ -179,244 +149,77 @@ ssh deploy@31.131.18.254 'cd /srv/gitea && docker compose restart'
- **Image:** `louislam/uptime-kuma:2` - **Image:** `louislam/uptime-kuma:2`
- **URL:** https://status.goodbrick.com.ua - **URL:** https://status.goodbrick.com.ua
- **Port:** 127.0.0.1:3001:3001 - **Port:** 127.0.0.1:3001:3001
- **Data:** `/srv/uptime-kuma/data`
**Restart:**
```bash
ssh deploy@31.131.18.254 'cd /srv/uptime-kuma && docker compose restart'
```
--- ---
### 5. Woodpecker CI ### 5. Woodpecker CI
- **Location:** `/srv/woodpecker` - **Location:** `/srv/woodpecker`
- **Containers:** `woodpecker-server`, `woodpecker-agent` - **Containers:** `woodpecker-server`, `woodpecker-agent`
- **Image:** `woodpeckerci/woodpecker-server:latest` (v2.8.3), `woodpeckerci/woodpecker-agent:latest` - **Image:** `woodpeckerci/woodpecker-server:latest` (v2.8.3+)
- **URL:** https://ci.goodbrick.com.ua - **URL:** https://ci.goodbrick.com.ua
- **Port:** 127.0.0.1:8000:8000 - **Port:** 127.0.0.1:8000:8000
- **Data:** `/srv/woodpecker/data`
**Config:** **Notes:**
```yaml - OAuth authentication via Gitea
environment:
# Server
- WOODPECKER_OPEN=true
- WOODPECKER_HOST=https://ci.goodbrick.com.ua
- WOODPECKER_GITEA=true
- WOODPECKER_GITEA_URL=https://git.goodbrick.com.ua
- WOODPECKER_GITEA_CLIENT=157422cf-7391-4fb0-8f2d-27083676dda6
- WOODPECKER_GITEA_SECRET=gto_2r3udrnmvtebt37v35bzl375eq5bumxqu2dpwwdfflbumnp5fy7q
- WOODPECKER_AGENT_SECRET=supersecrettoken123
- WOODPECKER_ADMIN=admin
# Agent
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=supersecrettoken123
```
**Status:** ✅ Working! Login with Gitea account.
**Important Notes:**
- Uses OAuth authentication via Gitea
- Agent connects to server on port 9000 (gRPC) - Agent connects to server on port 9000 (gRPC)
- Agent has access to Docker socket for running builds - Agent has Docker socket access for builds
- Version 2.8.3 fixes JavaScript security issues (v2.7.2 had SES errors) - v2.8.3+ required (older versions have SES JS errors)
**Restart:**
```bash
ssh deploy@31.131.18.254 'cd /srv/woodpecker && docker compose restart'
```
--- ---
## Common Commands ## Common Commands
### View all containers
```bash ```bash
# View all containers
ssh deploy@31.131.18.254 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"' ssh deploy@31.131.18.254 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
```
### View container logs # View container logs
```bash
ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50' ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50'
ssh deploy@31.131.18.254 'docker logs <container_name> -f' # Follow logs ssh deploy@31.131.18.254 'docker logs <container_name> -f'
```
### Restart a service # Restart a service
```bash
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart' ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart'
```
### Restart all services # Access PostgreSQL CLI
```bash
ssh deploy@31.131.18.254 'cd /srv/postgres && docker compose restart && \
cd /srv/gitea && docker compose restart && \
cd /srv/uptime-kuma && docker compose restart && \
cd /srv/woodpecker && docker compose restart && \
cd /srv/proxy && docker compose restart'
```
### Access PostgreSQL CLI
```bash
ssh deploy@31.131.18.254 'docker exec -it postgres psql -U app -d app' ssh deploy@31.131.18.254 'docker exec -it postgres psql -U app -d app'
```
### Check Docker network # Check Docker network
```bash
ssh deploy@31.131.18.254 'docker network inspect app-network' ssh deploy@31.131.18.254 'docker network inspect app-network'
ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"'
```
### Test container connectivity
```bash
ssh deploy@31.131.18.254 'docker exec <container1> ping -c 2 <container2>'
``` ```
--- ---
## Troubleshooting ## Troubleshooting
### Issue: Container can't reach another container ### Container can't reach another container
**Symptoms:** "connection refused", "no such host" **Symptoms:** "connection refused", "no such host"
**Check:**
```bash ```bash
# Verify all containers are in app-network # Check all containers are in app-network
ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"' ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"'
# Fix: ensure docker-compose has networks: app-network: external: true
``` ```
**Solution:** ### Caddy "502 Bad Gateway"
1. Ensure all docker-compose.yml files have: 1. Target service is down → `docker ps | grep <service>`
```yaml 2. Not in app-network → check networks
networks: 3. Wrong port/name in Caddyfile → check config
app-network:
external: true
```
2. Restart the affected service
--- ### Woodpecker blank page / JS errors
1. Update to v2.8.3+
### Issue: "dial tcp: lookup <service> on 127.0.0.11:53: no such host" 2. Ensure `flush_interval -1` in Caddy
**Cause:** Container not in app-network
**Solution:**
```bash
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose down && docker compose up -d'
```
---
### Issue: Caddy shows "502 Bad Gateway"
**Causes:**
1. Target service is down
2. Target service not in app-network
3. Wrong port/container name in Caddyfile
**Check:**
```bash
# Check if target is running
ssh deploy@31.131.18.254 'docker ps | grep <service>'
# Check Caddy logs
ssh deploy@31.131.18.254 'docker logs caddy --tail 50'
# Test direct connection
ssh deploy@31.131.18.254 'docker exec caddy wget -O- http://<container>:<port>'
```
---
### Issue: Woodpecker shows blank page or JavaScript errors
**Symptoms:** "SES Removing unpermitted intrinsics", SyntaxError
**Solution:**
1. Update to latest version (2.8.3+)
2. Ensure Caddy has `flush_interval -1` for SSE support
3. Clear browser cache (Ctrl+Shift+R) 3. Clear browser cache (Ctrl+Shift+R)
--- ### SSL certificate issues
Usually DNS propagation delay. Check `docker logs caddy | grep certificate` and `nslookup <domain>`.
### Issue: SSL certificate not working
**Cause:** Usually DNS propagation or Let's Encrypt rate limits
**Check:**
```bash
# Check Caddy logs
ssh deploy@31.131.18.254 'docker logs caddy | grep -i certificate'
# Check if domain resolves
nslookup <domain>
```
**Solution:** Wait for DNS propagation (up to 24h) or check DNS A records.
--- ---
## Security Notes ## TODO
1. **PostgreSQL:** - [ ] Настроить email для Gitea
- Not exposed to internet (127.0.0.1 only) - [ ] Настроить автобэкапы PostgreSQL
- Separate users for each application - [ ] Настроить алерты в Uptime Kuma
- Credentials stored in `/srv/postgres/CREDENTIALS.txt` (chmod 600)
2. **SSH:**
- Key-based authentication only
- User `deploy` has Docker permissions
3. **Docker Network:**
- All containers isolated in `app-network`
- No direct internet access except through Caddy
4. **Secrets:**
- Never commit credentials to git
- Store sensitive data in environment variables
- Use `.env` files (gitignored)
--- ---
## Next Steps / TODO **Last Updated:** 2026-02-10
- [ ] Deploy production app to `new.goodbrick.com.ua` (port 3100)
- [ ] Deploy development app to `dev.goodbrick.com.ua` (port 3200)
- [ ] Set up email for Gitea (optional)
- [ ] Configure Woodpecker pipelines for repositories
- [ ] Set up automatic backups for PostgreSQL databases
- [ ] Configure monitoring alerts in Uptime Kuma
---
## File Locations Summary
```
Server (31.131.18.254):
├── /srv/
│ ├── apps/
│ ├── gitea/
│ │ ├── docker-compose.yml
│ │ └── data/
│ ├── postgres/
│ │ ├── docker-compose.yml
│ │ ├── data/
│ │ └── CREDENTIALS.txt ← All DB credentials
│ ├── proxy/
│ │ ├── docker-compose.yml
│ │ ├── Caddyfile ← All routes
│ │ ├── data/
│ │ └── config/
│ ├── uptime-kuma/
│ │ ├── docker-compose.yml
│ │ └── data/
│ └── woodpecker/
│ ├── docker-compose.yml
│ └── data/
Local:
└── C:\Work\goodbrick\GBSite\
├── SERVER.md ← This file
└── README.md ← Project overview
```
---
**Last Updated:** 2026-02-09
**Maintained by:** Claude AI Assistant

View File

@@ -1,4 +1,5 @@
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();

View File

@@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://localhost:5224", "applicationUrl": "http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,6 +1,10 @@
services: services:
gb-dev-frontend: gb-dev-frontend:
build: ../frontend build:
context: ../frontend
args:
- NEXT_PUBLIC_POSTHOG_KEY=phc_pe0mP58n724h9eFxanbGIUsfMyS14gnAmr5tYez9V3Q
- NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
container_name: gb-dev-frontend container_name: gb-dev-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:

View File

@@ -0,0 +1,9 @@
services:
gb-local-backend:
build: ../backend
container_name: gb-local-backend
ports:
- "127.0.0.1:5000:5000"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__Default=Host=host.docker.internal;Port=5433;Database=dev_db;Username=dev_user;Password=dev_pass_vB6nM3qP8yW2rT9k

View File

@@ -1,6 +1,10 @@
services: services:
gb-prod-frontend: gb-prod-frontend:
build: ../frontend build:
context: ../frontend
args:
- NEXT_PUBLIC_POSTHOG_KEY=phc_pe0mP58n724h9eFxanbGIUsfMyS14gnAmr5tYez9V3Q
- NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
container_name: gb-prod-frontend container_name: gb-prod-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:

View File

@@ -1,2 +1,5 @@
# URL бэкенда для server-side запросов (внутри Docker network) # URL бэкенда для server-side запросов (внутри Docker network)
INTERNAL_API_URL=http://gb-prod-backend:5000 INTERNAL_API_URL=http://gb-prod-backend:5000
# URL бэкенда для проксирования /api/* в локальной разработке (не задавать на сервере!)
# LOCAL_API_URL=http://localhost:5000

View File

@@ -5,6 +5,8 @@ RUN npm ci
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN npm run build RUN npm run build

View File

@@ -2,6 +2,16 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
async rewrites() {
const apiUrl = process.env.LOCAL_API_URL;
if (!apiUrl) return [];
return [
{
source: "/api/:path*",
destination: `${apiUrl}/api/:path*`,
},
];
},
}; };
export default nextConfig; export default nextConfig;

View File

@@ -12,6 +12,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"posthog-js": "^1.343.2",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
@@ -1975,6 +1976,332 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/api-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-exporter-base": "0.208.0",
"@opentelemetry/otlp-transformer": "0.208.0",
"@opentelemetry/sdk-logs": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-exporter-base": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-transformer": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-transformer": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/sdk-logs": "0.208.0",
"@opentelemetry/sdk-metrics": "2.2.0",
"@opentelemetry/sdk-trace-base": "2.2.0",
"protobufjs": "^7.3.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@posthog/core": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.20.2.tgz",
"integrity": "sha512-aQhrUzOHYr0z/bkwDsJ5hXahdh6oyeWdx2/CHwR6vFG3eK07J69lbuGOj+HGOOxJP1eAdNnsk8J0fj1vqRA9+A==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/types": {
"version": "1.343.2",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.343.2.tgz",
"integrity": "sha512-8+RAnTOf/85b2NU+b4cpUZsuftRZQOFgTNDNCb1LyntXNgf7yD7KYxprC1rCayCZ/YpkmYy3xXPytBVYTXwdqg==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@radix-ui/number": { "node_modules/@radix-ui/number": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -3874,9 +4201,7 @@
"version": "20.19.33", "version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -3910,6 +4235,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/validate-npm-package-name": { "node_modules/@types/validate-npm-package-name": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
@@ -5319,6 +5651,17 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.6", "version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -5368,7 +5711,6 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@@ -5648,6 +5990,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.2.4", "version": "17.2.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
@@ -6632,6 +6983,12 @@
"node": "^12.20 || >= 14.13" "node": "^12.20 || >= 14.13"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/figures": { "node_modules/figures": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -7947,7 +8304,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
@@ -8486,6 +8842,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -9338,7 +9700,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -9440,6 +9801,27 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/posthog-js": {
"version": "1.343.2",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.343.2.tgz",
"integrity": "sha512-Aq5EG/knjf21Kx+rbxMHyyFknOfU49Z+b5WhbrQDiUQC5R2Dp/w1IPpQPNh0i0pScuPFyzG3YVisxRscFhuGqQ==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.208.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.20.2",
"@posthog/types": "1.343.2",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",
"preact": "^10.28.2",
"query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.1.0"
}
},
"node_modules/powershell-utils": { "node_modules/powershell-utils": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
@@ -9453,6 +9835,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/preact": {
"version": "10.28.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -9515,6 +9907,30 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -9555,6 +9971,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/query-selector-shadow-dom": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
"license": "MIT"
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -10345,7 +10767,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
@@ -10358,7 +10779,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -11203,7 +11623,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicorn-magic": { "node_modules/unicorn-magic": {
@@ -11414,11 +11833,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/web-vitals": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
"license": "Apache-2.0"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"

View File

@@ -13,6 +13,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"posthog-js": "^1.343.2",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { PostHogProvider } from "@/lib/posthog";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const geistSans = Geist({
@@ -27,7 +28,7 @@ export default function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
{children} <PostHogProvider>{children}</PostHogProvider>
</body> </body>
</html> </html>
); );

View File

@@ -1,3 +1,5 @@
import { PostHogTestButton } from "./posthog-test-button";
export default function Home() { export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center"> <div className="flex min-h-screen items-center justify-center">
@@ -13,6 +15,7 @@ export default function Home() {
<span>+</span> <span>+</span>
<span>.NET 9</span> <span>.NET 9</span>
</div> </div>
<PostHogTestButton />
</main> </main>
</div> </div>
); );

View File

@@ -0,0 +1,18 @@
"use client";
import { usePostHog } from "posthog-js/react";
export function PostHogTestButton() {
const posthog = usePostHog();
return (
<button
onClick={() =>
posthog.capture("my_custom_event", { property: "value" })
}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Send PostHog Test Event
</button>
);
}

View File

@@ -0,0 +1,52 @@
"use client";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
import { useEffect, Suspense } from "react";
import { usePathname, useSearchParams } from "next/navigation";
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://eu.i.posthog.com";
function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const ph = usePostHog();
useEffect(() => {
if (pathname && ph) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url += "?" + searchParams.toString();
}
ph.capture("$pageview", { $current_url: url });
}
}, [pathname, searchParams, ph]);
return null;
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (POSTHOG_KEY) {
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
capture_pageview: false, // manual pageview tracking for SPA
capture_pageleave: true,
});
}
}, []);
if (!POSTHOG_KEY) {
return <>{children}</>;
}
return (
<PHProvider client={posthog}>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
{children}
</PHProvider>
);
}

View File

@@ -0,0 +1,4 @@
Write-Host "Starting Docker backend on http://localhost:5000" -ForegroundColor Cyan
Write-Host "Requires: SSH tunnel running (scripts/local-tunnel.ps1)"
Write-Host ""
docker compose -f "$PSScriptRoot\..\deploy\docker-compose.local.yml" up --build

View File

@@ -0,0 +1,6 @@
Write-Host "Starting backend (dotnet run) on http://localhost:5000" -ForegroundColor Cyan
Write-Host "Requires: SSH tunnel running (scripts/local-tunnel.ps1)"
Write-Host ""
Push-Location "$PSScriptRoot\..\backend"
dotnet run --project src/GBSite.Api
Pop-Location

View File

@@ -0,0 +1,6 @@
Write-Host "Starting frontend on http://localhost:3000" -ForegroundColor Cyan
Write-Host "API proxy -> http://localhost:5000"
Write-Host ""
Push-Location "$PSScriptRoot\..\frontend"
npm run dev
Pop-Location

4
scripts/local-tunnel.ps1 Normal file
View File

@@ -0,0 +1,4 @@
Write-Host "SSH Tunnel: localhost:5433 -> server:5432 (dev_db)" -ForegroundColor Cyan
Write-Host "Press Ctrl+C to stop"
Write-Host ""
ssh -N -L 5433:127.0.0.1:5432 -o ServerAliveInterval=60 deploy@31.131.18.254