small fixes : cors + login loop

This commit is contained in:
senke 2026-02-07 20:36:48 +01:00
parent 1efafe0cc5
commit b1ed46b142
105 changed files with 3266 additions and 755 deletions

1604
MODULE.bazel.lock Normal file

File diff suppressed because it is too large Load diff

652
Makefile
View file

@ -3,644 +3,38 @@
# ============================================================================== # ==============================================================================
# Stack: Docker + Incus (LXD) Support # Stack: Docker + Incus (LXD) Support
# System: Linux / Bash # System: Linux / Bash
#
# Configuration: edit make/config.mk (ports, services, paths).
# Add new targets in make/*.mk or below.
# ============================================================================== # ==============================================================================
# --- Auto-Configuration ---
-include .env
# Shell setup
SHELL := /bin/bash SHELL := /bin/bash
.ONESHELL: .ONESHELL:
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
# --- Variables --- # --- Configuration (single source of truth) ---
PROJECT_NAME := veza include make/config.mk
COMPOSE_FILE := docker-compose.yml include make/ui.mk
COMPOSE_PROD := docker-compose.prod.yml
# Services # --- All feature modules ---
SERVICES := backend-api chat-server stream-server web haproxy include make/tools.mk
INFRA_SERVICES := postgres redis rabbitmq include make/infra.mk
include make/dev.mk
# Ports include make/build.mk
PORT_GO ?= 8080 include make/test.mk
PORT_CHAT ?= 3000 include make/services.mk
PORT_STREAM ?= 3001 include make/high.mk
PORT_WEB ?= 5173 include make/incus.mk
PORT_HAPROXY ?= 80 include make/help.mk
# Database & Infra
DB_USER ?= veza
DB_PASS ?= password
DB_NAME ?= veza
DB_HOST ?= localhost
DB_PORT ?= 5432
# Connection Strings
DATABASE_URL = postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
REDIS_URL = redis://localhost:6379
AMQP_URL = amqp://$(DB_USER):$(DB_PASS)@localhost:5672
# Directories
DIR_GO := veza-backend-api
DIR_CHAT := veza-chat-server
DIR_STREAM := veza-stream-server
DIR_WEB := apps/web
# Deployment
DEPLOY_TARGET ?= docker
INCUS_PROFILE := veza-profile
INCUS_NETWORK := veza-network
# --- Aesthetics & UI ---
BOLD := \033[1m
RED := \033[0;31m
GREEN := \033[0;32m
YELLOW := \033[0;33m
BLUE := \033[0;34m
PURPLE := \033[0;35m
CYAN := \033[0;36m
NC := \033[0m
ECHO_CMD = echo -e
# ============================================================================== # ==============================================================================
# HELP & DASHBOARD # PER-SERVICE CONVENIENCE (dev-*, test-*, lint-*, build-*)
# ============================================================================== # ==============================================================================
.PHONY: help # Usage: make dev-web, make test-backend-api, make lint-web, etc.
help: ## Show this dashboard # Add new services in make/config.mk (SERVICES, SERVICE_DIR_*, PORT_*).
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${PURPLE}⚡ VEZA MONOREPO CLI ⚡${NC}"
@$(ECHO_CMD) "================================================================="
@$(ECHO_CMD) "${BOLD}INFRASTRUCTURE:${NC}"
@printf " ${CYAN}%-15s${NC} %s\n" "Postgres" "${DATABASE_URL}"
@printf " ${CYAN}%-15s${NC} %s\n" "Redis" "${REDIS_URL}"
@printf " ${CYAN}%-15s${NC} %s\n" "RabbitMQ" "UI: http://localhost:15672 (veza/password)"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${GREEN}HIGH LEVEL COMMANDS:${NC}"
@grep -E '^[a-zA-Z0-9_-]+:.*?## \[HIGH\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${YELLOW}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${BLUE}INTERMEDIATE COMMANDS:${NC}"
@grep -E '^[a-zA-Z0-9_-]+:.*?## \[MID\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${CYAN}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${PURPLE}LOW LEVEL / DEBUG:${NC}"
@grep -E '^[a-zA-Z0-9_-]+:.*?## \[LOW\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${PURPLE}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
# ============================================================================== # ==============================================================================
# HIGH LEVEL COMMANDS
# ==============================================================================
.PHONY: setup stop-all restart-all clean deploy-docker deploy-incus status-full
setup: check-tools install-tools install-deps ## [HIGH] Full project initialization .PHONY: dev-web dev-backend-api dev-chat-server dev-stream-server
@$(ECHO_CMD) "${BOLD}${GREEN}✅ Setup Complete! Ready to rock with 'make dev'.${NC}" .PHONY: test-web test-backend-api test-chat-server test-stream-server
.PHONY: lint-web lint-backend-api lint-chat-server lint-stream-server
web-minimal: ## [HIGH] Start Veza Web Minimal Journey (Backend + Frontend + DB) # (targets defined in make/dev.mk and make/test.mk)
@./scripts/start_minimal.sh
stop-minimal: ## [HIGH] Stop Minimal Stack
@./scripts/stop_minimal.sh
stop-all: ## [HIGH] Stop all services (Docker + Local)
@$(ECHO_CMD) "${RED}🛑 Stopping all services...${NC}"
@docker compose -f $(COMPOSE_FILE) down 2>/dev/null || true
@docker compose -f $(COMPOSE_PROD) down 2>/dev/null || true
@$(MAKE) -s stop-local-services
@$(ECHO_CMD) "${GREEN}✅ All services stopped.${NC}"
restart-all: stop-all ## [HIGH] Restart all services
@$(ECHO_CMD) "${BLUE}🔄 Restarting all services...${NC}"
@$(MAKE) -s infra-up
@$(MAKE) -s dev
@$(ECHO_CMD) "${GREEN}✅ All services restarted.${NC}"
clean: ## [HIGH] Clean build artifacts and caches
@$(ECHO_CMD) "${YELLOW}🧹 Cleaning build artifacts...${NC}"
@rm -rf $(DIR_WEB)/node_modules/.cache
@rm -rf $(DIR_CHAT)/target/debug $(DIR_STREAM)/target/debug
@find . -type d -name "node_modules" -prune -o -type f -name "*.log" -delete
@$(ECHO_CMD) "${GREEN}✅ Clean complete.${NC}"
clean-deep: ## [HIGH] ⚠️ Nuclear Clean (Confirm required)
@read -p "${RED}Are you sure? This will delete ALL builds, volumes, and caches! [y/N]${NC} " ans && [ $${ans:-N} = y ]
@$(ECHO_CMD) "${RED}☢️ DESTROYING ARTIFACTS...${NC}"
@rm -rf $(DIR_WEB)/node_modules
@rm -rf $(DIR_CHAT)/target $(DIR_STREAM)/target
@docker compose -f $(COMPOSE_FILE) down -v 2>/dev/null || true
@docker compose -f $(COMPOSE_PROD) down -v 2>/dev/null || true
@$(ECHO_CMD) "${GREEN}System Cleaned.${NC}"
deploy-docker: build-all ## [HIGH] Deploy all services with Docker + HAProxy
@$(ECHO_CMD) "${BOLD}${BLUE}🐳 Deploying with Docker...${NC}"
@docker compose -f $(COMPOSE_PROD) up -d --build
@$(MAKE) -s wait-for-services
@$(ECHO_CMD) "${GREEN}✅ Deployment complete! Access via http://localhost:$(PORT_HAPROXY)${NC}"
deploy-incus: build-all-native ## [HIGH] Deploy all services with Incus containers (native, no Docker)
@$(ECHO_CMD) "${BOLD}${BLUE}📦 Deploying with Incus (native)...${NC}"
@$(MAKE) -s incus-setup-network
@$(MAKE) -s incus-deploy-infra
@$(MAKE) -s incus-deploy-all-native
@$(MAKE) -s incus-start-all
@$(ECHO_CMD) "${GREEN}✅ Incus deployment complete!${NC}"
@$(ECHO_CMD) "${BLUE}Access services at:${NC}"
@$(ECHO_CMD) " Backend API: http://10.10.10.2:8080"
@$(ECHO_CMD) " Chat Server: http://10.10.10.3:8081"
@$(ECHO_CMD) " Stream Server: http://10.10.10.4:3002"
@$(ECHO_CMD) " Web Frontend: http://10.10.10.5:80"
@$(ECHO_CMD) " HAProxy: http://10.10.10.6:80"
status-full: ## [HIGH] Show complete system status
@$(ECHO_CMD) "${BOLD}${CYAN}📊 SYSTEM STATUS${NC}"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Docker Containers:${NC}"
@docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "NAME|veza" || echo " No containers running"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Local Processes:${NC}"
@lsof -i :$(PORT_GO) -i :$(PORT_CHAT) -i :$(PORT_STREAM) -i :$(PORT_WEB) 2>/dev/null | grep LISTEN || echo " No local processes"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Incus Containers:${NC}"
@incus list veza- 2>/dev/null | grep -E "NAME|veza" || echo " No Incus containers"
@$(ECHO_CMD) ""
# ==============================================================================
# INTERMEDIATE COMMANDS
# ==============================================================================
.PHONY: start-service stop-service restart-service logs-service build-service
start-service: ## [MID] Start a specific service (usage: make start-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}"
@docker compose -f $(COMPOSE_PROD) up -d $(SERVICE) || \
$(MAKE) -s start-local-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) started.${NC}"
stop-service: ## [MID] Stop a specific service (usage: make stop-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${YELLOW}🛑 Stopping $(SERVICE)...${NC}"
@docker compose -f $(COMPOSE_PROD) stop $(SERVICE) 2>/dev/null || \
$(MAKE) -s stop-local-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) stopped.${NC}"
restart-service: stop-service ## [MID] Restart a specific service (usage: make restart-service SERVICE=backend-api)
@$(ECHO_CMD) "${BLUE}🔄 Restarting $(SERVICE)...${NC}"
@$(MAKE) -s start-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) restarted.${NC}"
logs-service: ## [MID] Show logs for a service (usage: make logs-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@docker compose -f $(COMPOSE_PROD) logs -f $(SERVICE) || \
$(ECHO_CMD) "${YELLOW}Service not running in Docker, check local logs${NC}"
build-service: ## [MID] Build a specific service (usage: make build-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}🔨 Building $(SERVICE)...${NC}"
@$(MAKE) -s build-$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) built.${NC}"
build-all: ## [MID] Build all services (Docker images)
@$(ECHO_CMD) "${BLUE}🔨 Building all services...${NC}"
@$(MAKE) -s build-backend-api
@$(MAKE) -s build-chat-server
@$(MAKE) -s build-stream-server
@$(MAKE) -s build-web
@$(ECHO_CMD) "${GREEN}✅ All services built.${NC}"
build-all-native: ## [MID] Build all services natively (for Incus)
@$(ECHO_CMD) "${BLUE}🔨 Building all services natively...${NC}"
@$(shell pwd)/config/incus/build-native.sh all
@$(ECHO_CMD) "${GREEN}✅ All services built natively.${NC}"
# ==============================================================================
# LOW LEVEL / DEBUG COMMANDS
# ==============================================================================
.PHONY: check-tools install-tools install-deps check-ports
check-tools: ## [LOW] Check required tools
@$(ECHO_CMD) "${BLUE}Checking core requirements...${NC}"
@for tool in docker go cargo npm; do \
command -v $$tool >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}$$tool is missing!${NC}"; exit 1; }; \
done
@$(ECHO_CMD) "${GREEN}✅ All tools present.${NC}"
check-tools-incus: ## [LOW] Check required tools for Incus deployment
@$(ECHO_CMD) "${BLUE}Checking Incus deployment requirements...${NC}"
@command -v incus >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ incus is missing! Install with: sudo snap install incus${NC}"; exit 1; }
@command -v go >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ go is missing!${NC}"; exit 1; }
@command -v cargo >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ cargo is missing!${NC}"; exit 1; }
@command -v npm >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ npm is missing!${NC}"; exit 1; }
@$(ECHO_CMD) "${GREEN}✅ All Incus tools present.${NC}"
install-tools: ## [LOW] Install Power User tools (Hot Reload, Linters)
@$(ECHO_CMD) "${BLUE}🛠️ Installing Dev Tools...${NC}"
@command -v air >/dev/null 2>&1 || go install github.com/air-verse/air@latest
@command -v cargo-watch >/dev/null 2>&1 || cargo install cargo-watch
@command -v sqlx >/dev/null 2>&1 || cargo install sqlx-cli --no-default-features --features native-tls,postgres
@$(ECHO_CMD) "${GREEN}✅ Tools installed.${NC}"
install-deps: ## [LOW] Install code dependencies
@$(ECHO_CMD) "${BLUE}📦 Installing dependencies...${NC}"
@$(ECHO_CMD) " -> [Go] Downloading modules..."
@(cd $(DIR_GO) && go mod download)
@$(ECHO_CMD) " -> [Rust Chat] Fetching crates..."
@(cd $(DIR_CHAT) && cargo fetch)
@$(ECHO_CMD) " -> [Rust Stream] Fetching crates..."
@(cd $(DIR_STREAM) && cargo fetch)
@$(ECHO_CMD) " -> [Web] Installing npm packages..."
@(cd $(DIR_WEB) && npm install --silent)
check-ports: ## [LOW] Check if ports are available
@$(ECHO_CMD) "${BLUE}🔍 Checking ports...${NC}"
@for port in $(PORT_GO) $(PORT_CHAT) $(PORT_STREAM) $(PORT_WEB); do \
if lsof -i :$$port -t >/dev/null 2>&1; then \
$(ECHO_CMD) "${YELLOW}⚠️ Port $$port is busy${NC}"; \
else \
$(ECHO_CMD) "${GREEN}✅ Port $$port is free${NC}"; \
fi; \
done
# ==============================================================================
# INFRASTRUCTURE
# ==============================================================================
.PHONY: infra-up infra-down wait-for-infra db-shell redis-shell db-migrate
infra-up: ## [MID] Start Docker Infra (with health checks)
@$(ECHO_CMD) "${BLUE}🐳 Starting Infrastructure...${NC}"
@docker compose -f $(COMPOSE_FILE) up -d
@$(MAKE) -s wait-for-infra
infra-down: ## [MID] Stop Docker Infra
@$(ECHO_CMD) "${BLUE}🛑 Stopping Infrastructure...${NC}"
@docker compose -f $(COMPOSE_FILE) down
wait-for-infra: ## [LOW] Wait for infrastructure to be ready
@printf "${BLUE}⏳ Waiting for services...${NC}"
@until docker compose -f $(COMPOSE_FILE) exec -T postgres pg_isready -U $(DB_USER) > /dev/null 2>&1; do printf "."; sleep 1; done
@until docker compose -f $(COMPOSE_FILE) exec -T redis redis-cli ping > /dev/null 2>&1; do printf "."; sleep 1; done
@$(ECHO_CMD) " ${GREEN}OK${NC}"
wait-for-services: ## [LOW] Wait for all application services
@printf "${BLUE}⏳ Waiting for services...${NC}"
@for service in backend-api chat-server stream-server web; do \
until docker compose -f $(COMPOSE_PROD) exec -T $$service echo "ready" > /dev/null 2>&1; do \
printf "."; sleep 1; \
done; \
done
@$(ECHO_CMD) " ${GREEN}OK${NC}"
db-shell: ## [MID] Connect to Postgres shell
@docker compose -f $(COMPOSE_FILE) exec postgres psql -U $(DB_USER) -d $(DB_NAME)
redis-shell: ## [MID] Connect to Redis shell
@docker compose -f $(COMPOSE_FILE) exec redis redis-cli
db-migrate: infra-up ## [MID] Run all database migrations
@$(ECHO_CMD) "${BLUE}🔄 Running Migrations...${NC}"
@$(ECHO_CMD) " -> [Go] Migrating..."
@(cd $(DIR_GO) && go run cmd/migrate_tool/main.go up || $(ECHO_CMD) "${YELLOW}Warning: Go migration failed${NC}")
@$(ECHO_CMD) " -> [Chat] Migrating..."
@(cd $(DIR_CHAT) && sqlx migrate run || $(ECHO_CMD) "${YELLOW}Warning: Chat migration failed${NC}")
@$(ECHO_CMD) " -> [Stream] Migrating..."
@(cd $(DIR_STREAM) && sqlx migrate run || $(ECHO_CMD) "${YELLOW}Warning: Stream migration failed${NC}")
@$(ECHO_CMD) "${GREEN}✅ Migrations done.${NC}"
# ==============================================================================
# DEVELOPMENT
# ==============================================================================
.PHONY: dev dev-backend stop-local-services start-local-service stop-local-service
dev: check-ports infra-up ## [HIGH] Start Everything (Detects Hot Reload tools)
@$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING HYBRID DEV ENVIRONMENT${NC}"
@$(ECHO_CMD) " Go: http://localhost:${PORT_GO}"
@$(ECHO_CMD) " Chat: http://localhost:${PORT_CHAT}"
@$(ECHO_CMD) " Web: http://localhost:${PORT_WEB}"
@$(ECHO_CMD) "${YELLOW}Hit Ctrl+C to stop all.${NC}"
@(trap 'kill 0' SIGINT; \
if command -v air >/dev/null; then \
$(ECHO_CMD) "${GREEN}[Go] Hot Reload Active (Air)${NC}" && cd $(DIR_GO) && air & \
else \
$(ECHO_CMD) "${YELLOW}[Go] Standard Run${NC}" && cd $(DIR_GO) && go run cmd/modern-server/main.go & \
fi; \
if command -v cargo-watch >/dev/null; then \
$(ECHO_CMD) "${GREEN}[Chat] Hot Reload Active${NC}" && cd $(DIR_CHAT) && cargo watch -x run -q & \
$(ECHO_CMD) "${GREEN}[Stream] Hot Reload Active${NC}" && cd $(DIR_STREAM) && cargo watch -x run -q & \
else \
$(ECHO_CMD) "${YELLOW}[Chat] Standard Run${NC}" && cd $(DIR_CHAT) && cargo run -q & \
$(ECHO_CMD) "${YELLOW}[Stream] Standard Run${NC}" && cd $(DIR_STREAM) && cargo run -q & \
fi; \
$(ECHO_CMD) "${GREEN}[Web] Starting Vite...${NC}" && cd $(DIR_WEB) && npm run dev & \
wait)
dev-backend: check-ports infra-up ## [MID] Start Backends Only (Hot Reload supported)
@$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING BACKEND ONLY${NC}"
@(trap 'kill 0' SIGINT; \
if command -v air >/dev/null; then cd $(DIR_GO) && air & else cd $(DIR_GO) && go run cmd/modern-server/main.go & fi; \
if command -v cargo-watch >/dev/null; then cd $(DIR_CHAT) && cargo watch -x run -q & else cd $(DIR_CHAT) && cargo run -q & fi; \
if command -v cargo-watch >/dev/null; then cd $(DIR_STREAM) && cargo watch -x run -q & else cd $(DIR_STREAM) && cargo run -q & fi; \
wait)
stop-local-services: ## [LOW] Stop all local processes
@pkill -f "air\|cargo watch\|npm run dev\|go run.*modern-server" 2>/dev/null || true
start-local-service: ## [LOW] Start a service locally
@case "$(SERVICE)" in \
backend-api) \
if command -v air >/dev/null; then cd $(DIR_GO) && air & else cd $(DIR_GO) && go run cmd/modern-server/main.go & fi ;; \
chat-server) \
if command -v cargo-watch >/dev/null; then cd $(DIR_CHAT) && cargo watch -x run -q & else cd $(DIR_CHAT) && cargo run -q & fi ;; \
stream-server) \
if command -v cargo-watch >/dev/null; then cd $(DIR_STREAM) && cargo watch -x run -q & else cd $(DIR_STREAM) && cargo run -q & fi ;; \
web) \
cd $(DIR_WEB) && npm run dev & ;; \
*) \
$(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}"; exit 1 ;; \
esac
stop-local-service: ## [LOW] Stop a local service
@case "$(SERVICE)" in \
backend-api) pkill -f "air\|go run.*modern-server" ;; \
chat-server|stream-server) pkill -f "cargo.*$(SERVICE)" ;; \
web) pkill -f "npm run dev\|vite" ;; \
*) $(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}" ;; \
esac
# ==============================================================================
# BUILD COMMANDS
# ==============================================================================
.PHONY: build-backend-api build-chat-server build-stream-server build-web
build-backend-api: ## [LOW] Build Go backend
@$(ECHO_CMD) "${BLUE}🔨 Building backend-api...${NC}"
@docker build -t $(PROJECT_NAME)-backend-api:latest -f $(DIR_GO)/Dockerfile.production $(DIR_GO) || \
$(ECHO_CMD) "${YELLOW}Using local Dockerfile...${NC}" && \
docker build -t $(PROJECT_NAME)-backend-api:latest -f $(DIR_GO)/Dockerfile $(DIR_GO)
build-chat-server: ## [LOW] Build Rust chat server
@$(ECHO_CMD) "${BLUE}🔨 Building chat-server...${NC}"
@docker build -t $(PROJECT_NAME)-chat-server:latest -f $(DIR_CHAT)/Dockerfile.production $(DIR_CHAT) || \
docker build -t $(PROJECT_NAME)-chat-server:latest -f $(DIR_CHAT)/Dockerfile $(DIR_CHAT)
build-stream-server: ## [LOW] Build Rust stream server
@$(ECHO_CMD) "${BLUE}🔨 Building stream-server...${NC}"
@docker build -t $(PROJECT_NAME)-stream-server:latest -f $(DIR_STREAM)/Dockerfile.production $(DIR_STREAM) || \
docker build -t $(PROJECT_NAME)-stream-server:latest -f $(DIR_STREAM)/Dockerfile $(DIR_STREAM)
build-web: ## [LOW] Build web frontend
@$(ECHO_CMD) "${BLUE}🔨 Building web...${NC}"
@docker build -t $(PROJECT_NAME)-web:latest -f $(DIR_WEB)/Dockerfile.production $(DIR_WEB) || \
docker build -t $(PROJECT_NAME)-web:latest -f $(DIR_WEB)/Dockerfile $(DIR_WEB)
# ==============================================================================
# INCUS / LXD DEPLOYMENT
# ==============================================================================
.PHONY: incus-setup-network incus-deploy-all incus-deploy-all-native incus-deploy-service incus-deploy-service-native incus-deploy-infra incus-start-all incus-stop-all incus-logs
incus-setup-network: ## [LOW] Setup Incus network profile
@$(ECHO_CMD) "${BLUE}📦 Setting up Incus network...${NC}"
@if ! incus network show $(INCUS_NETWORK) >/dev/null 2>&1; then \
$(ECHO_CMD) "Creating network $(INCUS_NETWORK)..."; \
incus network create $(INCUS_NETWORK) \
ipv4.address=10.10.10.1/24 \
ipv4.nat=true \
ipv4.dhcp=true \
dns.mode=managed \
dns.nameservers=8.8.8.8,1.1.1.1; \
else \
$(ECHO_CMD) "Updating network configuration..."; \
incus network set $(INCUS_NETWORK) ipv4.dhcp=true 2>/dev/null || true; \
incus network set $(INCUS_NETWORK) dns.mode=managed 2>/dev/null || true; \
incus network set $(INCUS_NETWORK) dns.nameservers=8.8.8.8,1.1.1.1 2>/dev/null || true; \
fi
@if ! incus profile show $(INCUS_PROFILE) >/dev/null 2>&1; then \
$(ECHO_CMD) "Creating profile $(INCUS_PROFILE)..."; \
incus profile create $(INCUS_PROFILE); \
incus profile device add $(INCUS_PROFILE) root disk path=/ pool=default 2>/dev/null || \
incus profile device add $(INCUS_PROFILE) root disk path=/ 2>/dev/null || true; \
incus profile device add $(INCUS_PROFILE) eth0 nic network=$(INCUS_NETWORK) 2>/dev/null || true; \
else \
$(ECHO_CMD) "Ensuring profile devices..."; \
if ! incus profile show $(INCUS_PROFILE) | grep -q "root:"; then \
incus profile device add $(INCUS_PROFILE) root disk path=/ pool=default 2>/dev/null || \
incus profile device add $(INCUS_PROFILE) root disk path=/ 2>/dev/null || true; \
fi; \
if ! incus profile show $(INCUS_PROFILE) | grep -q "eth0:"; then \
incus profile device add $(INCUS_PROFILE) eth0 nic network=$(INCUS_NETWORK) 2>/dev/null || true; \
fi; \
fi
@$(ECHO_CMD) "${GREEN}✅ Incus network ready.${NC}"
incus-deploy-all: incus-setup-network ## [MID] Deploy all services to Incus (legacy Docker method)
@$(ECHO_CMD) "${BLUE}📦 Deploying all services to Incus (Docker)...${NC}"
@$(MAKE) -s incus-deploy-service SERVICE=backend-api
@$(MAKE) -s incus-deploy-service SERVICE=chat-server
@$(MAKE) -s incus-deploy-service SERVICE=stream-server
@$(MAKE) -s incus-deploy-service SERVICE=web
@$(MAKE) -s incus-deploy-service SERVICE=haproxy
@$(ECHO_CMD) "${GREEN}✅ All services deployed to Incus.${NC}"
incus-deploy-all-native: incus-setup-network ## [MID] Deploy all services to Incus (native, no Docker) - excludes Rust services
@$(ECHO_CMD) "${BLUE}📦 Deploying all services to Incus (native, excluding Rust services)...${NC}"
@$(ECHO_CMD) "${YELLOW}⚠️ Note: chat-server and stream-server are excluded${NC}"
@$(MAKE) -s incus-deploy-service-native SERVICE=backend-api
@$(MAKE) -s incus-deploy-service-native SERVICE=web
@$(MAKE) -s incus-deploy-service-native SERVICE=haproxy
@$(ECHO_CMD) "${GREEN}✅ All services deployed to Incus.${NC}"
incus-deploy-service: ## [LOW] Deploy a service to Incus with Docker (usage: make incus-deploy-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}📦 Deploying $(SERVICE) to Incus (Docker)...${NC}"
@if incus list -c n --format csv | grep -q "^veza-$(SERVICE)$$"; then \
$(ECHO_CMD) "${YELLOW}Container exists, removing...${NC}"; \
incus delete veza-$(SERVICE) --force; \
fi
@incus init images:debian/13 veza-$(SERVICE) --profile $(INCUS_PROFILE)
@incus start veza-$(SERVICE)
@$(ECHO_CMD) "${BLUE}Installing Docker in container...${NC}"
@incus exec veza-$(SERVICE) -- bash -c "apt-get update && apt-get install -y docker.io docker-compose && systemctl enable docker && systemctl start docker" || true
@$(ECHO_CMD) "${GREEN}$(SERVICE) deployed.${NC}"
incus-deploy-service-native: ## [LOW] Deploy a service to Incus natively (usage: make incus-deploy-service-native SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}📦 Deploying $(SERVICE) to Incus (native)...${NC}"
@$(shell pwd)/config/incus/deploy-service-native.sh $(SERVICE)
incus-deploy-infra: incus-setup-network ## [LOW] Deploy infrastructure services (PostgreSQL, Redis)
@$(ECHO_CMD) "${BLUE}📦 Deploying infrastructure services...${NC}"
@$(MAKE) -s incus-deploy-service-native SERVICE=infra
@$(ECHO_CMD) "${BLUE}Waiting for infrastructure to be ready...${NC}"
@for i in $$(seq 1 30); do \
if incus exec veza-infra -- systemctl is-active postgresql >/dev/null 2>&1 && \
incus exec veza-infra -- systemctl is-active redis-server >/dev/null 2>&1; then \
$(ECHO_CMD) "${GREEN}✅ Infrastructure services ready${NC}"; \
break; \
fi; \
sleep 1; \
done
@$(ECHO_CMD) "${GREEN}✅ Infrastructure deployed.${NC}"
incus-start-all: ## [MID] Start all Incus services (excluding Rust services)
@$(ECHO_CMD) "${BLUE}🚀 Starting all Incus services (excluding Rust services)...${NC}"
@for service in backend-api; do \
if incus list -c n --format csv | grep -q "^veza-$$service$$"; then \
$(ECHO_CMD) "Starting veza-$$service..."; \
if incus exec veza-$$service -- systemctl start veza-$$service 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ veza-$$service started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ veza-$$service failed to start (check logs)${NC}"; \
fi; \
fi; \
done
@if incus list -c n --format csv | grep -q "^veza-web$$"; then \
$(ECHO_CMD) "Starting veza-web..."; \
if incus exec veza-web -- systemctl start apache2 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ Apache started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ Apache failed to start${NC}"; \
fi; \
fi
@if incus list -c n --format csv | grep -q "^veza-haproxy$$"; then \
$(ECHO_CMD) "Starting veza-haproxy..."; \
if incus exec veza-haproxy -- systemctl start haproxy 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ HAProxy started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ HAProxy failed to start${NC}"; \
fi; \
fi
@if incus list -c n --format csv | grep -q "^veza-infra$$"; then \
$(ECHO_CMD) "Starting infrastructure services..."; \
if incus exec veza-infra -- systemctl start postgresql 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ PostgreSQL started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ PostgreSQL failed to start${NC}"; \
fi; \
if incus exec veza-infra -- systemctl start redis-server 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ Redis started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ Redis failed to start${NC}"; \
fi; \
fi
@$(ECHO_CMD) "${GREEN}✅ All services started.${NC}"
@$(ECHO_CMD) "${BLUE}Run 'make incus-status' to check service status${NC}"
incus-stop-all: ## [MID] Stop all Incus containers
@$(ECHO_CMD) "${YELLOW}🛑 Stopping all Incus containers...${NC}"
@for container in $$(incus list -c n --format csv | grep veza-); do \
incus stop $$container 2>/dev/null || true; \
done
@$(ECHO_CMD) "${GREEN}✅ All Incus containers stopped.${NC}"
incus-status: ## [MID] Show status of all Incus services
@$(ECHO_CMD) "${BOLD}${CYAN}📊 INCUS DEPLOYMENT STATUS${NC}"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Containers:${NC}"
@incus list veza- --format table 2>/dev/null || echo " No containers found"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Service Status:${NC}"
@for service in backend-api chat-server stream-server; do \
if incus list -c n --format csv 2>/dev/null | grep -q "^veza-$$service$$"; then \
STATUS=$$(incus exec veza-$$service -- systemctl is-active veza-$$service 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-$$service: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-$$service: $$STATUS${NC}"; \
fi; \
fi; \
done
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-web$$"; then \
STATUS=$$(incus exec veza-web -- systemctl is-active apache2 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-web (Apache): active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-web (Apache): $$STATUS${NC}"; \
fi; \
fi
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-haproxy$$"; then \
STATUS=$$(incus exec veza-haproxy -- systemctl is-active haproxy 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-haproxy: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-haproxy: $$STATUS${NC}"; \
fi; \
fi
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-infra$$"; then \
PG_STATUS=$$(incus exec veza-infra -- systemctl is-active postgresql 2>/dev/null || echo "inactive"); \
REDIS_STATUS=$$(incus exec veza-infra -- systemctl is-active redis-server 2>/dev/null || echo "inactive"); \
if [ "$$PG_STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ PostgreSQL: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ PostgreSQL: $$PG_STATUS${NC}"; \
fi; \
if [ "$$REDIS_STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ Redis: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ Redis: $$REDIS_STATUS${NC}"; \
fi; \
fi
@$(ECHO_CMD) ""
incus-logs: ## [LOW] Show logs from Incus container (usage: make incus-logs SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@incus exec veza-$(SERVICE) -- journalctl -f
# ==============================================================================
# TEST & QUALITY
# ==============================================================================
.PHONY: test test-tmt lint fmt status
test-tmt: ## [MID] Run Unified TMT Pipeline
@$(ECHO_CMD) "${BLUE}🧪 Running TMT Pipeline...${NC}"
@command -v tmt >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ tmt is missing! Install with 'pip install tmt'${NC}"; exit 1; }
@tmt run
test: infra-up ## [MID] Run All Tests (Fastest strategy)
@$(ECHO_CMD) "${BLUE}🧪 Running Tests...${NC}"
@$(ECHO_CMD) " [Go] Unit Tests..."
@(cd $(DIR_GO) && go test ./... -short)
@$(ECHO_CMD) " [Rust] Unit Tests..."
@(cd $(DIR_CHAT) && cargo test --lib -q)
@(cd $(DIR_STREAM) && cargo test --lib -q)
@$(ECHO_CMD) " [Web] Unit Tests..."
@(cd $(DIR_WEB) && npm run test -- --run)
@$(ECHO_CMD) "${GREEN}✅ All tests passed.${NC}"
lint: ## [MID] Lint everything
@$(ECHO_CMD) "${BLUE}🔍 Linting Codebase...${NC}"
@(cd $(DIR_CHAT) && cargo clippy -- -D warnings) || true
@(cd $(DIR_STREAM) && cargo clippy -- -D warnings) || true
@(cd $(DIR_GO) && golangci-lint run ./...) || true
@(cd $(DIR_WEB) && npm run lint) || true
fmt: ## [MID] Format everything
@$(ECHO_CMD) "${BLUE}✨ Formatting...${NC}"
@(cd $(DIR_GO) && go fmt ./...)
@(cd $(DIR_CHAT) && cargo fmt)
@(cd $(DIR_STREAM) && cargo fmt)
@(cd $(DIR_WEB) && npm run format) || true
status: ## [MID] Show system health & stats
@$(ECHO_CMD) "${BOLD}DOCKER STATS:${NC}"
@docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" 2>/dev/null | grep -E "NAME|veza" || echo "No containers running"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}LOCAL PORTS:${NC}"
@lsof -i :$(PORT_GO) -i :$(PORT_CHAT) -i :$(PORT_STREAM) -i :$(PORT_WEB) 2>/dev/null | grep LISTEN || echo "No apps listening."

View file

@ -3,6 +3,9 @@
# API Configuration # API Configuration
# Base URL for the REST API (can be absolute URL or path starting with /) # Base URL for the REST API (can be absolute URL or path starting with /)
# DEV (veza.fr or localhost): use /api/v1 so the Vite proxy forwards to the backend.
# - Same origin => cookies are sent => login and /auth/me work. Using http://localhost:8080
# from veza.fr:5173 is cross-origin => cookies not sent => 401 and redirect loop.
VITE_API_URL=/api/v1 VITE_API_URL=/api/v1
# WebSocket Configuration # WebSocket Configuration

View file

@ -1,5 +1,5 @@
# Configuration API pour développement local # Configuration API pour développement local
# Backend Go tourne sur le port 8080 # Backend Go tourne sur le port 8080
VITE_API_URL=http://localhost:8080/api/v1 VITE_API_URL=/api/v1
VITE_WS_URL=ws://localhost:8081/ws VITE_WS_URL=ws://localhost:8081/ws
VITE_STREAM_URL=ws://localhost:8082/stream VITE_STREAM_URL=ws://localhost:8082/stream

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

View file

@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}

View file

@ -107,26 +107,18 @@ export function App() {
}, [setTheme, theme, language, setLanguage]); }, [setTheme, theme, language, setLanguage]);
// P1.2: Initialize auth state before rendering app // P1.2: Initialize auth state before rendering app
// This prevents race condition where router renders before auth is checked // With httpOnly cookies we cannot read tokens in JS; always call refreshUser()
// so getMe() is used to verify auth (cookies sent automatically).
useEffect(() => { useEffect(() => {
const initAuth = async () => { const initAuth = async () => {
try { try {
// Check if user has tokens
const { hasTokens } = await import('@/services/tokenStorage').then(
(m) => ({ hasTokens: m.TokenStorage.hasTokens() })
);
if (hasTokens) {
// Wait for auth check to complete
await refreshUser(); await refreshUser();
}
} catch (error) { } catch (error) {
logger.error('[App] Auth initialization failed', { logger.error('[App] Auth initialization failed', {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined, stack: error instanceof Error ? error.stack : undefined,
}); });
} finally { } finally {
// Always set ready, even if auth check fails
setIsAuthReady(true); setIsAuthReady(true);
} }
}; };

View file

@ -75,6 +75,25 @@ const parseEnv = () => {
// Variables d'environnement validées // Variables d'environnement validées
const validatedEnv = parseEnv(); const validatedEnv = parseEnv();
// En dev, alerter si l'API est en cross-origin : les cookies ne seront pas envoyés (SameSite),
// ce qui provoque 401 après login et redirections en boucle. Utiliser VITE_API_URL=/api/v1 (proxy).
if (import.meta.env.DEV && typeof window !== 'undefined') {
const apiUrl = validatedEnv.VITE_API_URL;
if (apiUrl.startsWith('http')) {
try {
const apiOrigin = new URL(apiUrl).origin;
if (window.location.origin !== apiOrigin) {
logger.warn(
'[Config] API is cross-origin: cookies will not be sent, login may fail or redirect in a loop. Use VITE_API_URL=/api/v1 so the Vite proxy is used (same origin).',
{ apiOrigin, pageOrigin: window.location.origin }
);
}
} catch {
// ignore invalid URL
}
}
}
// Export de l'objet env avec types // Export de l'objet env avec types
export const env = { export const env = {
API_URL: validatedEnv.VITE_API_URL, API_URL: validatedEnv.VITE_API_URL,

View file

@ -10,31 +10,22 @@ export const useLogin = () => {
return useMutation({ return useMutation({
mutationFn: async (credentials: LoginRequest) => { mutationFn: async (credentials: LoginRequest) => {
// loginStore appelle déjà loginService et met à jour le store const loginResponse = await loginStore(credentials);
// Il attend aussi que la persistance soit complète const user = loginResponse.user;
await loginStore(credentials);
// Vérifier que le store est bien mis à jour après la persistance // Populate React Query cache so the app has user data immediately
const { isAuthenticated } = useAuthStore.getState();
if (!isAuthenticated) {
// Attendre un peu plus et réessayer
await new Promise((resolve) => setTimeout(resolve, 100));
const retryState = useAuthStore.getState();
if (!retryState.isAuthenticated) {
throw new Error(
'Login failed: user not authenticated after persistence',
);
}
}
// Fetch user data and update React Query cache
const user = await getMe();
queryClient.setQueryData(['user', 'me'], user); queryClient.setQueryData(['user', 'me'], user);
return { // Optionally refresh from /auth/me; do not fail login if this fails (e.g. backend delay)
user, getMe()
isAuthenticated, .then((freshUser) => {
}; queryClient.setQueryData(['user', 'me'], freshUser);
})
.catch(() => {
// Keep using user from login response
});
return { user, isAuthenticated: true };
}, },
}); });
}; };

View file

@ -7,10 +7,27 @@ import { OAuthButton } from '../components/OAuthButton';
import { useLogin } from '../hooks/useLogin'; import { useLogin } from '../hooks/useLogin';
import type { LoginFormData } from '../types'; import type { LoginFormData } from '../types';
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
import { formatErrorMessage as formatApiErrorMessage } from '@/utils/apiErrorHandler';
import type { ApiError } from '@/schemas/apiSchemas';
import { CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { AlertCircle } from 'lucide-react'; import { AlertCircle } from 'lucide-react';
import { AuthLayout } from '../components/AuthLayout'; import { AuthLayout } from '../components/AuthLayout';
function getLoginErrorMessage(error: unknown): string {
if (error == null) return '';
if (typeof error === 'object' && error !== null && 'message' in error && 'code' in error) {
return formatApiErrorMessage(error as ApiError);
}
if (error instanceof Error) {
const msg = error.message?.toLowerCase() ?? '';
if (msg.includes('invalid credentials') || msg.includes('401')) return 'Incorrect email or password';
if (msg.includes('email not verified')) return "Your email is not verified. Check your inbox.";
if (msg.includes('network')) return 'Connection error. Check your internet.';
return error.message || 'An error occurred. Please try again.';
}
return String(error);
}
export function LoginPage() { export function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthenticated, isLoading } = useAuthStore(); const { isAuthenticated, isLoading } = useAuthStore();
@ -117,23 +134,6 @@ export function LoginPage() {
window.location.href = `/api/v1/auth/oauth/${provider}`; window.location.href = `/api/v1/auth/oauth/${provider}`;
}; };
const formatErrorMessage = (error: Error | null): string => {
if (!error) return '';
const message = error.message.toLowerCase();
const errorString = error.toString().toLowerCase();
if (message.includes('invalid credentials') || message.includes('401') || errorString.includes('401')) {
return 'Incorrect email or password';
}
if (message.includes('email not verified')) {
return "Your email is not verified. Check your inbox.";
}
if (message.includes('network')) {
return 'Connection error. Check your internet.';
}
return 'An error occurred. Please try again.';
};
return ( return (
<AuthLayout <AuthLayout
title="Welcome Back" title="Welcome Back"
@ -156,7 +156,7 @@ export function LoginPage() {
{error && ( {error && (
<div className="bg-destructive/10 border border-destructive/30 text-destructive px-4 py-3 rounded-lg text-sm flex items-center gap-2"> <div className="bg-destructive/10 border border-destructive/30 text-destructive px-4 py-3 rounded-lg text-sm flex items-center gap-2">
<AlertCircle className="w-4 h-4 flex-shrink-0" /> <AlertCircle className="w-4 h-4 flex-shrink-0" />
<p>{formatErrorMessage(error)}</p> <p>{getLoginErrorMessage(error)}</p>
</div> </div>
)} )}
@ -183,18 +183,18 @@ export function LoginPage() {
/> />
</div> </div>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between gap-3 text-sm min-w-0">
<div className="flex items-center gap-2"> <label htmlFor="remember_me" className="flex items-center gap-2 text-muted-foreground cursor-pointer min-w-0 flex-shrink">
<input <input
type="checkbox" type="checkbox"
id="remember_me" id="remember_me"
checked={remember_me} checked={remember_me}
onChange={(e) => setRemember_me(e.target.checked)} onChange={(e) => setRemember_me(e.target.checked)}
className="h-4 w-4 rounded border-white/10 bg-black/20 text-primary focus:ring-primary/50" className="h-4 w-4 rounded border-white/10 bg-black/20 text-primary focus:ring-primary/50 flex-shrink-0"
/> />
<label htmlFor="remember_me" className="text-muted-foreground">Remember me</label> <span className="truncate">Remember me</span>
</div> </label>
<Link to="/forgot-password" className="text-primary hover:underline">Forgot password?</Link> <Link to="/forgot-password" className="text-primary hover:underline flex-shrink-0">Forgot password?</Link>
</div> </div>
<AuthButton type="submit" loading={loading} className="w-full bg-gradient-to-r from-cyan-600 to-magenta-600 hover:from-cyan-500 hover:to-magenta-500 text-white border-0 shadow-lg shadow-cyan-900/20"> <AuthButton type="submit" loading={loading} className="w-full bg-gradient-to-r from-cyan-600 to-magenta-600 hover:from-cyan-500 hover:to-magenta-500 text-white border-0 shadow-lg shadow-cyan-900/20">

View file

@ -6,6 +6,7 @@ import {
logout as logoutService, logout as logoutService,
getMe, getMe,
type LoginRequest, type LoginRequest,
type LoginResponse,
type RegisterRequest, type RegisterRequest,
} from '@/services/api/auth'; } from '@/services/api/auth';
import { TokenStorage } from '@/services/tokenStorage'; import { TokenStorage } from '@/services/tokenStorage';
@ -26,7 +27,7 @@ export interface AuthState {
} }
export interface AuthActions { export interface AuthActions {
login: (credentials: LoginRequest) => Promise<void>; login: (credentials: LoginRequest) => Promise<LoginResponse>;
register: (userData: RegisterRequest) => Promise<void>; register: (userData: RegisterRequest) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
logoutLocal: () => void; // Logout local sans appel API (pour éviter les boucles) logoutLocal: () => void; // Logout local sans appel API (pour éviter les boucles)
@ -54,25 +55,22 @@ export const useAuthStore = create<AuthStore>()(
login: async (credentials: LoginRequest) => { login: async (credentials: LoginRequest) => {
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
// Le service auth gère déjà le stockage des tokens const response = await loginService(credentials);
// Action 4.1.1.5: user field removed - user data managed by React Query
// Response contains user data but we don't store it (React Query handles that)
await loginService(credentials);
// Mettre à jour l'état de manière atomique pour éviter les problèmes de timing
set({ set({
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
error: null, error: null,
}); });
// Récupérer le token CSRF après login
csrfService.refreshToken().catch((error) => { csrfService.refreshToken().catch((error) => {
logger.warn('Failed to fetch CSRF token after login', { logger.warn('Failed to fetch CSRF token after login', {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined, stack: error instanceof Error ? error.stack : undefined,
}); });
}); });
return response;
} catch (error: unknown) { } catch (error: unknown) {
set({ set({
error: parseApiError(error), error: parseApiError(error),
@ -176,26 +174,15 @@ export const useAuthStore = create<AuthStore>()(
refreshUser: async () => { refreshUser: async () => {
// Action 4.3.1.2: Simplified using React Query - no manual promise deduplication needed // Action 4.3.1.2: Simplified using React Query - no manual promise deduplication needed
// React Query's useUser hook handles deduplication automatically at the query level
const currentState = useAuthStore.getState(); const currentState = useAuthStore.getState();
if (!TokenStorage.hasTokens()) {
// CRITIQUE FIX #2: Ne réinitialiser que si on n'était pas déjà authentifié
if (!currentState.isAuthenticated) {
set({ isAuthenticated: false, isLoading: false });
}
return;
}
// CRITIQUE FIX #2: Ne pas réinitialiser isAuthenticated si on était déjà authentifié // CRITIQUE FIX #2: Ne pas réinitialiser isAuthenticated si on était déjà authentifié
// Cela évite les problèmes de timing après le login et la navigation
const hasAuth = currentState.isAuthenticated; const hasAuth = currentState.isAuthenticated;
// SECURITY: With httpOnly cookies, hasTokens() is always false in JS.
// Always try getMe() to verify auth; cookies are sent automatically.
set({ isLoading: true }); set({ isLoading: true });
try { try {
// Verify authentication by calling getMe() // Verify authentication by calling getMe() (cookies sent automatically)
// User data is managed by React Query (useUser hook), not stored here
// React Query will deduplicate this call if useUser hook is already fetching
await getMe(); await getMe();
set({ set({
isAuthenticated: true, isAuthenticated: true,

View file

@ -45,8 +45,6 @@ export const handlers = [
http.post('*/api/v1/auth/login', async () => { http.post('*/api/v1/auth/login', async () => {
return HttpResponse.json({ return HttpResponse.json({
access_token: 'mock_access_token_generic',
refresh_token: 'mock_refresh_token_generic',
user: { user: {
id: 1, id: 1,
username: 'StorybookUser', username: 'StorybookUser',
@ -54,6 +52,11 @@ export const handlers = [
created_at: '2024-01-01T00:00:00Z', created_at: '2024-01-01T00:00:00Z',
avatar_url: 'https://i.pravatar.cc/150?u=1', avatar_url: 'https://i.pravatar.cc/150?u=1',
}, },
token: {
access_token: 'mock_access_token_generic',
refresh_token: 'mock_refresh_token_generic',
expires_in: 3600,
},
}); });
}), }),

View file

@ -1247,10 +1247,11 @@ apiClient.interceptors.response.use(
} }
// INT-AUTH-003: Détecter 401 et refresh automatiquement // INT-AUTH-003: Détecter 401 et refresh automatiquement
// EXCLURE l'endpoint /auth/refresh pour éviter les boucles infinies // EXCLURE /auth/refresh et /auth/logout pour éviter les boucles.
// EXCLURE aussi /auth/logout car si le logout échoue, on ne doit pas rafraîchir le token // EXCLURE /auth/me : 401 = non connecté ; ne pas tenter de refresh ni rediriger (sinon boucle rechargement).
const isRefreshEndpoint = originalRequest?.url?.includes('/auth/refresh'); const isRefreshEndpoint = originalRequest?.url?.includes('/auth/refresh');
const isLogoutEndpoint = originalRequest?.url?.includes('/auth/logout'); const isLogoutEndpoint = originalRequest?.url?.includes('/auth/logout');
const isAuthMeEndpoint = originalRequest?.url?.includes('/auth/me');
// INT-AUTH-003: Handle 401 and 400 on /auth/refresh endpoint - token expired/revoked/invalid, logout and redirect // INT-AUTH-003: Handle 401 and 400 on /auth/refresh endpoint - token expired/revoked/invalid, logout and redirect
// FIX: Gérer aussi les erreurs 400 (Bad Request) qui indiquent un refresh token invalide // FIX: Gérer aussi les erreurs 400 (Bad Request) qui indiquent un refresh token invalide
@ -1340,7 +1341,8 @@ apiClient.interceptors.response.use(
originalRequest && originalRequest &&
!originalRequest._retry && !originalRequest._retry &&
!isRefreshEndpoint && !isRefreshEndpoint &&
!isLogoutEndpoint !isLogoutEndpoint &&
!isAuthMeEndpoint
) { ) {
// INT-AUTH-003: Éviter les refresh multiples simultanés // INT-AUTH-003: Éviter les refresh multiples simultanés
if (isRefreshing) { if (isRefreshing) {
@ -1732,22 +1734,22 @@ apiClient.interceptors.response.use(
const apiError = parseApiError(error); const apiError = parseApiError(error);
// Action 3.2.1.4: Auth errors redirect to login // Action 3.2.1.4: Auth errors redirect to login
// Handle 401 errors that didn't trigger refresh (e.g., no originalRequest, already retried, etc.) // isAuthMeEndpoint déjà défini plus haut : on ne redirige pas pour /auth/me (401 = non connecté, pas de redirect)
// EXCLURE aussi /auth/logout pour éviter les boucles if (
if (status === 401 && !isRefreshEndpoint && !isLogoutEndpoint && typeof window !== 'undefined') { status === 401 &&
!isRefreshEndpoint &&
!isLogoutEndpoint &&
!isAuthMeEndpoint &&
typeof window !== 'undefined'
) {
const errorCategory = getErrorCategory(apiError); const errorCategory = getErrorCategory(apiError);
if (errorCategory === 'authentication') { if (errorCategory === 'authentication') {
// Clear tokens
TokenStorage.clearTokens(); TokenStorage.clearTokens();
csrfService.clearToken(); csrfService.clearToken();
// Clear auth store state
// FIX: Utiliser logoutLocal() pour éviter les boucles infinies
import('@/features/auth/store/authStore') import('@/features/auth/store/authStore')
.then(({ useAuthStore }) => { .then(({ useAuthStore }) => {
const store = useAuthStore.getState(); const store = useAuthStore.getState();
// Utiliser logoutLocal() au lieu de logout() pour éviter les appels API
// qui déclencheraient à nouveau le refresh
store.logoutLocal(); store.logoutLocal();
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
@ -1756,12 +1758,10 @@ apiClient.interceptors.response.use(
}); });
}); });
// Store error message for display after redirect
sessionStorage.setItem( sessionStorage.setItem(
'auth_error', 'auth_error',
'Votre session a expiré. Veuillez vous reconnecter.', 'Votre session a expiré. Veuillez vous reconnecter.',
); );
// Redirect to login
window.location.href = '/login'; window.location.href = '/login';
} }
} }

View file

@ -242,6 +242,17 @@ export function parseApiError(error: unknown): ApiError {
}; };
} }
if (status === 423) {
const data = responseData as { message?: string } | null;
return {
code: 423,
message:
data?.message ||
'This action cannot be completed right now. The resource may be locked or your account may be temporarily restricted. Please try again later.',
timestamp: new Date().toISOString(),
};
}
// Erreur HTTP sans format standardisé // Erreur HTTP sans format standardisé
const data = responseData as { message?: string } | null; const data = responseData as { message?: string } | null;
return { return {
@ -357,7 +368,10 @@ export function formatErrorMessage(
error: ApiError, error: ApiError,
includeRequestId: boolean = false, includeRequestId: boolean = false,
): string { ): string {
let message = error.message; const baseMessage =
typeof error.message === 'string' ? error.message : 'An error occurred';
let message = baseMessage;
// Si l'erreur a des détails de validation, les inclure // Si l'erreur a des détails de validation, les inclure
if ( if (
@ -366,9 +380,17 @@ export function formatErrorMessage(
error.details.length > 0 error.details.length > 0
) { ) {
const detailsMessages = error.details const detailsMessages = error.details
.map((detail) => `${detail.field}: ${detail.message}`) .map((detail) => {
const f =
typeof detail.field === 'string' ? detail.field : String(detail.field);
const m =
typeof detail.message === 'string'
? detail.message
: String(detail.message);
return `${f}: ${m}`;
})
.join(', '); .join(', ');
message = `${error.message} (${detailsMessages})`; message = `${baseMessage} (${detailsMessages})`;
} }
// Action 5.3.1.1: Always include request_id when requested (not just in development) // Action 5.3.1.1: Always include request_id when requested (not just in development)

View file

@ -7,8 +7,11 @@ import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const isProduction = mode === 'production' const isProduction = mode === 'production'
const projectRoot = path.resolve(__dirname)
return { return {
// Ensure dev server and dep scan use apps/web only (avoid picking up storybook-static when run from monorepo root)
root: projectRoot,
plugins: [ plugins: [
react(), react(),
// Bundle analyzer for production builds // Bundle analyzer for production builds
@ -39,6 +42,15 @@ export default defineConfig(({ mode }) => {
server: { server: {
port: 5173, port: 5173,
host: true, host: true,
// Allow dev access via local domain names (e.g. /etc/hosts: 127.0.0.1 veza.fr)
allowedHosts: ['veza.fr', 'veza.com', 'veza.talas.fr', 'veza.talas.com'],
// Exclude Storybook build output from watch and fs access so dep scan never touches it
watch: {
ignored: ['**/storybook-static/**', '**/dist_verification/**'],
},
fs: {
deny: ['**/storybook-static/**', '**/dist_verification/**'],
},
// P2.1: Proxy API requests to backend in development // P2.1: Proxy API requests to backend in development
// This eliminates CORS issues in dev by making all requests same-origin // This eliminates CORS issues in dev by making all requests same-origin
proxy: { proxy: {
@ -78,7 +90,9 @@ export default defineConfig(({ mode }) => {
}, },
// Standard optimization settings usually work best // Standard optimization settings usually work best
optimizeDeps: { optimizeDeps: {
include: ['react', 'react-dom'] include: ['react', 'react-dom'],
// Only scan from app entry; avoid storybook-static (and other build outputs) being picked up as entries
entries: ['index.html', 'src/main.tsx'],
}, },
} }
}) })

View file

@ -0,0 +1,193 @@
# Audit visuel exhaustif du frontend Veza
**Date:** 2026-02-07
**Objectif:** Identifier les causes précises de la "mocheté" perçue (layout, composants, couleurs, contrastes, typographie, cohérence).
---
## 1. Résumé exécutif
Le frontend souffre de **plusieurs facteurs cumulés** : palette daccents incohérente (teal + magenta/purple + vert + rouge), manque de profondeur (cartes trop plates), éléments "placeholder" visibles (ex. "0%" en rouge partout), barre de lecture disproportionnée, et typographie potentiellement dégradée (Rajdhani + erreurs glyph). Les correctifs ciblent des fichiers et variables précis ci-dessous.
---
## 2. Palette et couleurs
### 2.1 Incohérence des couleurs daccent
| Contexte | Couleur utilisée | Fichier / token | Problème |
|----------|------------------|-----------------|----------|
| Élément actif sidebar, bouton play, "NETWORK STABLE" | **Teal / cyan** (primary) | `--primary: oklch(0.75 0.18 195)` dans `index.css` | Cohérent comme accent principal. |
| Badges sidebar (Live Sessions 3, Channels 12) | **Magenta / violet** (`secondary`) | `Sidebar.tsx` L195 : `bg-secondary/20 text-secondary` ; `--secondary: oklch(0.65 0.25 330)` | **Hors palette** par rapport au teal ; donne une impression de "troisième couleur" non intégrée. |
| Pourcentages positifs, "ACTIVE", "NETWORK STABLE" (dot) | **Vert** (lime/success) | `StatCard.tsx` (lime), succès sémantique | Un **vert** distinct du teal pour "positif" crée une **double convention** (teal vs vert) pour des états similaires. |
| Tendances négatives, "Expired Warranty", Sign Out | **Rouge** (destructive) | `AdminDashboardStatCard.tsx`, `Sidebar.tsx` (Live icon) | Correct sémantiquement mais **trop présent** si utilisé aussi pour "0%" (voir §3). |
**Recommandations :**
- **Badges sidebar** : remplacer `secondary` (magenta) par une variante du primary (ex. `primary` ou `cyan-500`) ou un token dédié "badge" aligné sur la charte. Fichier : `apps/web/src/components/layout/Sidebar.tsx` (L194201).
- **États "positif"** : unifier soit sur teal, soit sur vert, et documenter (ex. teal = interactif/actif, vert = succès/variation positive uniquement).
### 2.2 Manque de profondeur (cartes, fonds)
- **Cartes dashboard** : variante `glass` ou `default` avec bordures/ombres très légères (`border-white/5`, `shadow-black/5`). Fichiers : `components/ui/card.tsx` (variants `glass`, `default`), `AdminDashboardStatCard.tsx`, `AdminDashboardTrafficCard.tsx`.
- **Recherche header** : `bg-muted/50 border border-border` — contraste faible avec le fond, la zone "Search Network..." se fond dans le fond.
- **Dark mode** : `--card: oklch(0.18 0.02 265)` très proche de `--background: oklch(0.15 0.02 265)` dans `index.css` (.dark), donc **peu de relief**.
**Recommandations :**
- Augmenter légèrement la différence luminance card vs background (ex. card à 0.200.22, background 0.15).
- Donner aux cartes une bordure ou une ombre un peu plus marquée (ex. `border-border` plus visible, `shadow-lg` avec teinte légère).
- Barre de recherche : fond ou bordure un peu plus marqués pour laffordance (ex. `bg-card` ou `border-white/10`).
### 2.3 Fichiers à modifier (couleurs)
- `apps/web/src/index.css` : variables `.dark` (--card, --background), éventuellement --border.
- `apps/web/src/components/layout/Sidebar.tsx` : classes des badges (remplacer secondary par primary/cyan).
- `apps/web/src/components/layout/Header.tsx` : input search (classes bg/border).
---
## 3. Composants "placeholder" ou trompeurs
### 3.1 "0%" en rouge sur toutes les cartes (Admin)
- **Comportement** : `AdminDashboardStatCard` affiche un badge de tendance (`trend`) avec `trend > 0` → vert, sinon **rouge**. Si lAPI ne renvoie pas de tendances (ou renvoie 0), on obtient **"0%" en rouge sur chaque carte**.
- **Fichiers** : `AdminDashboardStatCard.tsx` (L4657), `AdminDashboardView.tsx` (L4850 : `trend: stats.trends?.users` etc.), `useAdminDashboardView.ts` (stats venant de lAPI).
- **Impact** : ressemble à une erreur ou à une donnée non implémentée, ce qui renforce limpression dinterface inachevée.
**Recommandations :**
- Ne pas afficher le badge de tendance quand `trend === undefined` (ou null). Afficher "0%" seulement si la métrique a du sens (ex. "0% de variation" explicite).
- Si `trend === 0`, éviter le style "erreur" (rouge) : utiliser un style neutre (muted) ou masquer.
### 3.2 Graphique "Traffic Flux"
- **Comportement** : `AdminDashboardTrafficCard` utilise des barres **aléatoires** (`Math.random()`) et des labels factices ("SYS_INIT", "BUFFERING_NODES...", "LIVE_DATA"). Aucune donnée réelle, aucun axe Y, pas de grille lisible.
- **Fichier** : `apps/web/src/components/admin/admin-dashboard-view/AdminDashboardTrafficCard.tsx`.
- **Impact** : lintitulé "HOLOGRAPHIC STREAMING INTERFACE" promet un élément avancé alors que le rendu est clairement un placeholder.
**Recommandations :**
- Soit brancher de vraies données + axes + légende claire, soit remplacer par un message du type "Données à venir" ou un skeleton, et éviter un faux graphique.
### 3.3 Bouton "Sign In" alors que lutilisateur est connecté
- **Constat** (daprès captures) : un bouton "Sign In" peut apparaître à côté dun utilisateur déjà identifié (ex. "vezadev").
- **À vérifier** : `Header.tsx` / `Navbar.tsx` — affichage conditionnel du bouton de connexion vs profil. Sassurer que "Sign In" nest affiché que lorsque `!isAuthenticated`.
---
## 4. Layout et espacement
### 4.1 Barre de lecture (MiniPlayer / GlobalPlayer)
- **Taille** : `MiniPlayer` utilise `h-24` (96px) en barre fixe. Les contrôles (notamment le bouton play) sont très mis en avant (teal, grande taille).
- **Impact** : la barre occupe une part importante de la hauteur et attire trop lattention sur les pages où la lecture nest pas le focus (ex. Gear Locker, Academy, Admin).
- **Fichiers** : `apps/web/src/components/player/MiniPlayer.tsx` (L36 : `h-24`), `PlayerControls.tsx`, `PlayPauseButton.tsx`.
**Recommandations :**
- Réduire la hauteur sur desktop (ex. `h-20` ou `h-18`) et/ou rendre le contraste du bouton play un peu moins fort (même teal mais moins saturé ou plus petit).
- Barre de progression : déjà fine ; envisager une hauteur un peu plus visible pour la partie "remplie" (accessibilité + lisibilité).
### 4.2 Espacement entre sections (sidebar)
- **Constat** : les blocs "MY STUDIO", "VEZA NETWORK", etc. ont un espacement vertical serré entre le titre de section et le premier lien.
- **Fichier** : `apps/web/src/components/layout/Sidebar.tsx` (structure des sections).
- **Recommandation** : ajouter un peu de marge au-dessus des titres de section (ex. `mt-4` ou `space-y-1` entre titre et premier item) pour clarifier la hiérarchie.
### 4.3 Cartes dashboard (Command Center)
- **Constat** : les quatre petites cartes (Tracks Listened, Messages Sent, etc.) sont serrées ; le texte et les pourcentages peuvent sembler denses.
- **Fichiers** : vues dashboard qui utilisent `StatCard` ou équivalent ; grille (ex. `grid-cols-4`, `gap-6`).
- **Recommandation** : garder les layout primitives (pas de valeurs arbitraires) mais ajuster `gap` ou `padding` des cartes pour plus de respiration (ex. `p-6` déjà présent, éventuellement `gap-8`).
---
## 5. Typographie
### 5.1 Police Rajdhani
- **Usage** : `--font-sans: 'Rajdhani', ...` dans `index.css` (@theme inline). Utilisée pour le corps et une grande partie de lUI.
- **Problème connu** : les erreurs console "downloadable font: Glyph bbox was incorrect" (Rajdhani) indiquent des **glyphes mal déclarés** dans le fichier de police. Conséquences possibles : rendu moins net, décalages, ou fallback partiel vers une autre police.
- **Fichiers** : `index.html` (lien Google Fonts), `apps/web/src/index.css` (--font-sans).
**Recommandations :**
- Vérifier la source de la police (version, subset) et si possible utiliser une version mise à jour ou un autre fournisseur.
- En parallèle, prévoir un fallback explicite (ex. `'Rajdhani', 'Inter', system-ui, sans-serif`) pour limiter les effets si Rajdhani pose problème.
### 5.2 Hiérarchie et lisibilité
- **Texte secondaire** : `text-muted-foreground` (oklch(0.70 0.01 265) en dark) peut être trop proche du fond sur certains écrans, ce qui réduit le contraste et la hiérarchie.
- **Recommandation** : augmenter très légèrement la luminance ou le contraste de `--muted-foreground` en dark (ex. 0.720.75) et valider avec un outil WCAG.
---
## 6. Contrastes et accessibilité
### 6.1 Éléments à faible contraste
- **Bordures** : `border-white/5`, `border-white/10` — très subtiles, peu visibles pour certains utilisateurs.
- **Icônes** : petites icônes en `text-muted-foreground` dans les cartes et le player ; contraste insuffisant pour une identification rapide.
- **Progress bar** (player) : barre de progression très fine ; partie "remplie" (teal) lisible, mais le rail peut manquer de contraste.
**Recommandations :**
- Utiliser au minimum `border-white/10` pour les séparations importantes, et réserver `white/5` aux détails purement décoratifs.
- Icônes secondaires : envisager une couleur un peu plus claire (ex. `text-foreground/70`) ou une taille légèrement supérieure pour les actions importantes.
### 6.2 Champs mot de passe en HTTP
- **Constat** : message navigateur "Password fields present on an insecure (http://) page" en dev. Ce nest pas un problème de design mais de contexte (HTTPS en prod recommandé).
---
## 7. Cohérence et système de design
### 7.1 Double jeu de tokens (KŌDŌ vs design-tokens)
- **index.css** : variables type `--primary`, `--cyan-500`, `--card`, etc. (oklch).
- **design-tokens.css** : variables `--kodo-void`, `--kodo-cyan`, `--kodo-text-dim`, etc. (rgb).
- **Composants** : certains utilisent `primary` / `cyan-500`, dautres `text-kodo-cyan`, `bg-kodo-steel`, etc. (ex. `StatCard.tsx` : `text-kodo-cyan`, `bg-kodo-steel/10`).
- **Risque** : dérives de teintes entre les deux systèmes et maintenance plus difficile.
**Recommandation :**
- À moyen terme, unifier sur un seul jeu de tokens (idéalement celui de `index.css` étendu en Tailwind) et migrer progressivement les `kodo-*` vers les tokens sémantiques (primary, muted, etc.).
### 7.2 Variantes de cartes
- **card.tsx** propose plusieurs variants : `default`, `elevated`, `ghost`, `outline`, `muted`, `glass`, `interactive`, `glow`, `glowMagenta`, `spotlight`. Lusage de `glass` partout (admin, etc.) donne un rendu très uniforme et plat.
- **Recommandation** : utiliser `default` ou `elevated` pour les cartes de contenu principal afin de retrouver un peu dombre et de relief, et réserver `glass` à des blocs spécifiques (panneaux, overlays).
---
## 8. Synthèse des actions prioritaires
| Priorité | Action | Fichier(s) principal(aux) |
|----------|--------|----------------------------|
| P0 | Unifier la couleur des badges sidebar (primary au lieu de secondary) | `Sidebar.tsx` |
| P0 | Ne pas afficher le badge "0%" en rouge quand trend est 0 ou undefined ; style neutre ou masqué | `AdminDashboardStatCard.tsx` |
| P1 | Donner plus de relief aux cartes (card vs background, bordure/ombre) | `index.css` (.dark), `card.tsx` |
| P1 | Remplacer ou clarifier le graphique "Traffic Flux" (données réelles ou placeholder explicite) | `AdminDashboardTrafficCard.tsx` |
| P1 | Vérifier laffichage "Sign In" quand lutilisateur est connecté | `Header.tsx`, `Navbar.tsx` |
| P2 | Réduire la prééminence visuelle du player (hauteur, taille du bouton play) | `MiniPlayer.tsx`, `PlayerControls.tsx` |
| P2 | Améliorer laffordance de la barre de recherche (fond/bordure) | `Header.tsx` |
| P2 | Renforcer le contraste du texte secondaire et des bordures en dark | `index.css` |
| P3 | Unifier les tokens (kodo-* vs primary/muted) et documenter la charte | `design-tokens.css`, `index.css`, composants |
| P3 | Corriger ou contourner les glyphes Rajdhani (source font, fallback) | `index.html`, `index.css` |
---
## 9. Fichiers modifiables (référence rapide)
- **Couleurs / thème** : `apps/web/src/index.css` (variables :root et .dark).
- **Sidebar** : `apps/web/src/components/layout/Sidebar.tsx`.
- **Header** : `apps/web/src/components/layout/Header.tsx`.
- **Cartes** : `apps/web/src/components/ui/card.tsx` ; `AdminDashboardStatCard.tsx`, `AdminDashboardTrafficCard.tsx`.
- **Player** : `apps/web/src/components/player/MiniPlayer.tsx`, `PlayerControls.tsx`, `PlayPauseButton.tsx`.
- **Dashboard** : `apps/web/src/components/admin/admin-dashboard-view/AdminDashboardView.tsx`, `useAdminDashboardView.ts`.
- **Typographie** : `apps/web/index.html` (fonts), `apps/web/src/index.css` (--font-sans, @layer base).
Cet audit peut servir de base pour des tickets (P0 → P3) et pour une checklist avant refonte visuelle plus large.

View file

@ -0,0 +1,111 @@
# Orchestration et gestion du monorepo Veza
Ce document décrit la structure du Makefile modulaire et recommande des outils open source pour orchestrer le projet pendant son développement.
---
## 1. Structure actuelle du Makefile
### Organisation
| Fichier | Rôle |
|--------|------|
| **Makefile** | Point dentrée unique ; inclut les fragments et définit le goal par défaut (`help`) |
| **make/config.mk** | **Source de vérité** : services, ports, chemins (SERVICE_DIR_*, PORT_*, COMPOSE_*, ROOT, etc.) |
| **make/ui.mk** | Couleurs et `ECHO_CMD` |
| **make/help.mk** | Cible `help` et dashboard |
| **make/tools.mk** | `check-tools`, `install-tools`, `install-deps`, `check-ports` |
| **make/infra.mk** | `infra-up`, `infra-down`, `db-migrate`, `db-shell`, `redis-shell` |
| **make/dev.mk** | `dev`, `dev-backend`, `dev-web`, `dev-<service>`, `stop-local-services` |
| **make/build.mk** | `build-*`, `build-all`, `build-all-native`, `build-service` |
| **make/test.mk** | `test`, `test-<service>`, `lint`, `lint-<service>`, `fmt`, `status` |
| **make/services.mk** | `start-service`, `stop-service`, `restart-service`, `logs-service` (Docker) |
| **make/high.mk** | `setup`, `stop-all`, `clean`, `deploy-docker`, `deploy-incus`, `status-full`, `web-minimal` |
| **make/incus.mk** | Toutes les cibles Incus (network, deploy, start, stop, status, logs) |
### Personnalisation
- **Ajouter un service** : dans `make/config.mk`, ajouter le nom dans `SERVICES`, puis définir `SERVICE_DIR_<nom>` et `PORT_<nom>` si besoin. Adapter les règles dans les `.mk` qui font du case-by-service (ex. `dev.mk`, `build.mk`).
- **Changer un port** : dans `make/config.mk` ou via `.env` (ex. `PORT_web=3000`).
- **Changer un chemin** : modifier `SERVICE_DIR_<service>` dans `make/config.mk`.
- **Override global** : utiliser `.env` (chargé par `config.mk` avec `-include .env`).
### Cibles par service
- `make dev-web`, `make dev-backend-api`, `make dev-chat-server`, `make dev-stream-server`
- `make test-web`, `make test-backend-api`, …
- `make lint-web`, `make lint-backend-api`, …
- `make build-service SERVICE=backend-api`
---
## 2. Outils open source recommandés
En complément (ou en partie en remplacement) du Makefile, ces outils peuvent aider à orchestrer le monorepo.
### 2.1 Turborepo (JS/TS)
- **Site** : [turbo.build](https://turbo.build)
- **Rôle** : Cache des tâches et pipeline de build/test/lint pour les workspaces npm.
- **Atouts** : Cache distant optionnel, parallélisation, `turbo run build --filter=./apps/web`, intégration CI simple.
- **Limites** : Ciblé npm/pnpm workspaces (apps/web, packages/design-system). Les backends Go/Rust restent gérés par le Makefile ou des scripts.
- **Usage typique** : `turbo run build`, `turbo run test --filter=web`, `turbo run lint`.
À envisager si tu veux un cache et un graphe de tâches fiable pour la partie Node/TS, tout en gardant Make pour linfra et les backends.
---
### 2.2 Nx
- **Site** : [nx.dev](https://nx.dev)
- **Rôle** : Monorepo “full stack” : graphe de dépendances, cache, affected (build/test uniquement sur ce qui a changé), plugins (React, Node, Go, etc.).
- **Atouts** : Très flexible, “affected” puissant, support multi-langage (y compris Go via plugins ou custom targets).
- **Limites** : Plus lourd à configurer et à maintenir que Turborepo ; courbe dapprentissage plus forte.
Utile si le monorepo grossit encore et que tu veux “affected”, cache partagé et une seule interface pour build/test/lint (y compris backends).
---
### 2.3 Just (command runner)
- **Site** : [github.com/casey/just](https://github.com/casey/just)
- **Rôle** : Remplacer une partie des cibles Make par un fichier `justfile` (syntaxe plus lisible, arguments nommés, pas de tabs).
- **Atouts** : Scripts lisibles, facile à partager entre devs, multiplateforme (Windows possible).
- **Limites** : Pas de vrai “graphe de tâches” ni cache ; plutôt un complément ou un remplacement léger du Make pour les commandes métier.
Exemple : `just dev`, `just test web`, `just deploy docker`.
---
### 2.4 Mise (ex-asdf)
- **Site** : [mise.jdx.dev](https://mise.jdx.dev)
- **Rôle** : Gestion des versions des runtimes (Node, Go, Rust, Python) au niveau du repo ou de la machine.
- **Atouts** : Un seul outil pour `.node-version`, `.go-version`, etc. ; reproductibilité des envs entre dev et CI.
- **Limites** : Ne remplace pas le Makefile ; il assure seulement que les bonnes versions sont actives.
Recommandé pour figer Node/Go/Rust et éviter les “ça marche chez moi”.
---
### 2.5 Tâche (Task)
- **Site** : [taskfile.dev](https://taskfile.dev)
- **Rôle** : Alternative au Make en YAML, avec variables, includes, dépendances entre tâches.
- **Atouts** : Syntaxe YAML, parallélisation, bon pour des pipelines déclaratifs.
- **Limites** : Un outil de plus ; si le Make modulaire te convient, pas obligatoire.
---
## 3. Synthèse
| Besoin | Outil suggéré |
|--------|----------------|
| Un seul endroit pour config (ports, services, chemins) | **make/config.mk** (déjà en place) |
| Cache + pipeline pour JS/TS uniquement | **Turborepo** |
| Affected + cache + multi-langage (y compris Go/Rust) | **Nx** |
| Commandes lisibles, peu de dépendance à Make | **Just** |
| Versions Node/Go/Rust reproductibles | **Mise** |
| Tout garder en Make mais mieux structuré | **Makefile + make/*.mk** (actuel) |
Recommandation pragmatique : garder le **Makefile modulaire** comme entrée principale (infra, dev, deploy, Incus). Si la partie frontend/npm devient la plus coûteuse (builds, tests, lint), ajouter **Turborepo** pour les workspaces npm et appeler `turbo` depuis le Makefile si besoin (ex. `make test` → infra-up + turbo run test + tests Go/Rust). Pour les versions des runtimes, **Mise** (ou asdf) dans le repo et en CI améliore la reproductibilité sans toucher au reste.

94
go.work.sum Normal file
View file

@ -0,0 +1,94 @@
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=
github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY=
github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM=
github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U=
github.com/containerd/nri v0.6.1/go.mod h1:7+sX3wNx+LR7RzhjnJiUkFDhn18P5Bg/0VnJ/uXpRJM=
github.com/containerd/ttrpc v1.2.4/go.mod h1:ojvb8SJBSch0XkqNO0L0YX/5NxR3UnVk2LzFKBK0upc=
github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
github.com/containerd/zfs v1.1.0/go.mod h1:oZF9wBnrnQjpWLaPKEinrx3TQ9a+W/RJO7Zb41d8YLE=
github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
github.com/containernetworking/plugins v1.2.0/go.mod h1:/VjX4uHecW5vVimFa1wkG4s+r/s9qIfPdqlLF4TW8c4=
github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131/go.mod h1:eVWQJVQ67aMvYhpkDwaH2Goy2vo6v8JCMfGXfQ9sPtw=
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3L6/Py8F+QgAfeiuY87xuYGwCDqRFrvCzw=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU=
k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I=
k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8=
k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU=
k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs=
k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto=
tags.cncf.io/container-device-interface/specs-go v0.7.0/go.mod h1:hMAwAbMZyBLdmYqWgYcKH0F/yctNpV3P35f+/088A80=

View file

@ -0,0 +1,7 @@
export type {
CheckoutViewProps,
CheckoutFormState,
} from './types';
export { CheckoutView } from './CheckoutView';
export { CheckoutViewSkeleton } from './CheckoutViewSkeleton';
export { useCheckoutView } from './useCheckoutView';

View file

@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import { http, HttpResponse } from 'msw';
import {
PlaybackHeatmap,
PlaybackHeatmapSkeleton,
} from './PlaybackHeatmap';
import type { PlaybackHeatmapData } from './playback-heatmap';
const meta: Meta<typeof PlaybackHeatmap> = {
title: 'Components/Features/Streaming/PlaybackHeatmap',
component: PlaybackHeatmap,
parameters: { layout: 'padded' },
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
const mockHeatmap: PlaybackHeatmapData = {
track_id: '123',
track_duration: 180,
segment_size: 5,
total_sessions: 10,
max_intensity: 1.0,
generated_at: '2024-01-01T00:00:00Z',
segments: [
{ start_time: 0, end_time: 5, listen_count: 10, skip_count: 0, intensity: 1.0, average_play_time: 5 },
{ start_time: 5, end_time: 10, listen_count: 8, skip_count: 2, intensity: 0.8, average_play_time: 4 },
{ start_time: 10, end_time: 15, listen_count: 5, skip_count: 3, intensity: 0.5, average_play_time: 2.5 },
],
};
/** Données chargées via MSW (GET /api/v1/tracks/:id/playback/heatmap). */
export const Default: Story = {
args: { trackId: '123' },
};
/** Taille de segment personnalisée */
export const CustomSegmentSize: Story = {
args: { trackId: '123', segmentSize: 10 },
};
/** État de chargement (skeleton) */
export const Loading: Story = {
name: 'Chargement',
render: () => <PlaybackHeatmapSkeleton />,
};
/** Données vides (aucun segment) */
export const Empty: Story = {
name: 'Empty',
args: {
trackId: '123',
initialHeatmap: {
...mockHeatmap,
segments: [],
total_sessions: 0,
},
},
};
/** Erreur chargement */
export const Error: Story = {
name: 'Error',
parameters: {
msw: {
handlers: [
http.get('*/api/v1/tracks/:id/playback/heatmap', () =>
HttpResponse.json(
{ success: false, error: { message: 'Failed to load heatmap' } },
{ status: 500 },
),
),
],
},
},
args: { trackId: '123' },
};

46
make/build.mk Normal file
View file

@ -0,0 +1,46 @@
# ==============================================================================
# BUILD (Docker images and native for Incus)
# ==============================================================================
.PHONY: build-backend-api build-chat-server build-stream-server build-web
.PHONY: build-all build-all-native build-service
build-backend-api: ## [LOW] Build Go backend Docker image
@$(ECHO_CMD) "${BLUE}🔨 Building backend-api...${NC}"
@docker build -t $(PROJECT_NAME)-backend-api:latest -f $(ROOT)/$(SERVICE_DIR_backend-api)/Dockerfile.production $(ROOT)/$(SERVICE_DIR_backend-api) || \
($(ECHO_CMD) "${YELLOW}Using local Dockerfile...${NC}" && \
docker build -t $(PROJECT_NAME)-backend-api:latest -f $(ROOT)/$(SERVICE_DIR_backend-api)/Dockerfile $(ROOT)/$(SERVICE_DIR_backend-api))
build-chat-server: ## [LOW] Build Rust chat server Docker image
@$(ECHO_CMD) "${BLUE}🔨 Building chat-server...${NC}"
@docker build -t $(PROJECT_NAME)-chat-server:latest -f $(ROOT)/$(SERVICE_DIR_chat-server)/Dockerfile.production $(ROOT)/$(SERVICE_DIR_chat-server) || \
docker build -t $(PROJECT_NAME)-chat-server:latest -f $(ROOT)/$(SERVICE_DIR_chat-server)/Dockerfile $(ROOT)/$(SERVICE_DIR_chat-server))
build-stream-server: ## [LOW] Build Rust stream server Docker image
@$(ECHO_CMD) "${BLUE}🔨 Building stream-server...${NC}"
@docker build -t $(PROJECT_NAME)-stream-server:latest -f $(ROOT)/$(SERVICE_DIR_stream-server)/Dockerfile.production $(ROOT)/$(SERVICE_DIR_stream-server) || \
docker build -t $(PROJECT_NAME)-stream-server:latest -f $(ROOT)/$(SERVICE_DIR_stream-server)/Dockerfile $(ROOT)/$(SERVICE_DIR_stream-server))
build-web: ## [LOW] Build web frontend Docker image
@$(ECHO_CMD) "${BLUE}🔨 Building web...${NC}"
@docker build -t $(PROJECT_NAME)-web:latest -f $(ROOT)/$(SERVICE_DIR_web)/Dockerfile.production $(ROOT)/$(SERVICE_DIR_web) || \
docker build -t $(PROJECT_NAME)-web:latest -f $(ROOT)/$(SERVICE_DIR_web)/Dockerfile $(ROOT)/$(SERVICE_DIR_web))
build-all: ## [MID] Build all services (Docker images)
@$(ECHO_CMD) "${BLUE}🔨 Building all services...${NC}"
@$(MAKE) -s build-backend-api
@$(MAKE) -s build-chat-server
@$(MAKE) -s build-stream-server
@$(MAKE) -s build-web
@$(ECHO_CMD) "${GREEN}✅ All services built.${NC}"
build-all-native: ## [MID] Build all services natively (for Incus)
@$(ECHO_CMD) "${BLUE}🔨 Building all services natively...${NC}"
@$(INCUS_SCRIPTS)/build-native.sh all
@$(ECHO_CMD) "${GREEN}✅ All services built natively.${NC}"
build-service: ## [MID] Build a specific service (usage: make build-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi
@$(ECHO_CMD) "${BLUE}🔨 Building $(SERVICE)...${NC}"
@$(MAKE) -s build-$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) built.${NC}"

64
make/config.mk Normal file
View file

@ -0,0 +1,64 @@
# ==============================================================================
# VEZA MONOREPO - CONFIGURATION (single source of truth)
# ==============================================================================
# Edit this file to add services, change ports, or paths.
# Override via .env or environment (e.g. PORT_WEB=3000 make dev).
# ==============================================================================
-include .env
# --- Project ---
PROJECT_NAME ?= veza
ROOT ?= $(CURDIR)
# --- Compose ---
COMPOSE_FILE ?= docker-compose.yml
COMPOSE_PROD ?= docker-compose.prod.yml
# --- Services (space-separated; must match keys in SERVICE_DIRS / SERVICE_PORTS)
SERVICES := backend-api chat-server stream-server web haproxy
INFRA_SERVICES := postgres redis rabbitmq
# --- Service → Directory mapping (customize paths here)
SERVICE_DIR_backend-api := veza-backend-api
SERVICE_DIR_chat-server := veza-chat-server
SERVICE_DIR_stream-server := veza-stream-server
SERVICE_DIR_web := apps/web
SERVICE_DIR_haproxy :=
# --- Ports (override with PORT_<SERVICE>=... from .env)
PORT_backend-api ?= 8080
PORT_chat-server ?= 3000
PORT_stream-server ?= 3001
PORT_web ?= 5173
PORT_haproxy ?= 80
# Legacy names for backward compatibility
PORT_GO ?= $(PORT_backend-api)
PORT_CHAT ?= $(PORT_chat-server)
PORT_STREAM ?= $(PORT_stream-server)
PORT_WEB ?= $(PORT_web)
PORT_HAPROXY ?= $(PORT_haproxy)
# --- Database & Infra ---
DB_USER ?= veza
DB_PASS ?= password
DB_NAME ?= veza
DB_HOST ?= localhost
DB_PORT ?= 5432
DATABASE_URL = postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
REDIS_URL = redis://localhost:6379
AMQP_URL = amqp://$(DB_USER):$(DB_PASS)@localhost:5672
# --- Incus ---
DEPLOY_TARGET ?= docker
INCUS_PROFILE ?= veza-profile
INCUS_NETWORK ?= veza-network
INCUS_SCRIPTS ?= $(ROOT)/config/incus
# --- NPM workspaces (from root package.json; used for install-deps / lint scope)
NPM_WORKSPACES ?= apps/web packages/design-system
# --- Scripts ---
SCRIPTS ?= $(ROOT)/scripts

80
make/dev.mk Normal file
View file

@ -0,0 +1,80 @@
# ==============================================================================
# DEVELOPMENT (local run with optional hot reload)
# ==============================================================================
.PHONY: dev dev-backend dev-web dev-backend-api dev-chat-server dev-stream-server
.PHONY: stop-local-services start-local-service stop-local-service
dev: check-ports infra-up ## [HIGH] Start Everything (Detects Hot Reload tools)
@$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING HYBRID DEV ENVIRONMENT${NC}"
@$(ECHO_CMD) " Go: http://localhost:$(PORT_backend-api)"
@$(ECHO_CMD) " Chat: http://localhost:$(PORT_chat-server)"
@$(ECHO_CMD) " Web: http://localhost:$(PORT_web)"
@$(ECHO_CMD) "${YELLOW}Hit Ctrl+C to stop all.${NC}"
@(trap 'kill 0' SIGINT; \
if command -v air >/dev/null; then \
$(ECHO_CMD) "${GREEN}[Go] Hot Reload Active (Air)${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & \
else \
$(ECHO_CMD) "${YELLOW}[Go] Standard Run${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & \
fi; \
if command -v cargo-watch >/dev/null; then \
$(ECHO_CMD) "${GREEN}[Chat] Hot Reload Active${NC}" && cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & \
$(ECHO_CMD) "${GREEN}[Stream] Hot Reload Active${NC}" && cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q & \
else \
$(ECHO_CMD) "${YELLOW}[Chat] Standard Run${NC}" && cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q & \
$(ECHO_CMD) "${YELLOW}[Stream] Standard Run${NC}" && cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q & \
fi; \
$(ECHO_CMD) "${GREEN}[Web] Starting Vite...${NC}" && cd $(ROOT)/$(SERVICE_DIR_web) && npm run dev & \
wait)
dev-backend: check-ports infra-up ## [MID] Start Backends Only (Hot Reload supported)
@$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING BACKEND ONLY${NC}"
@(trap 'kill 0' SIGINT; \
if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & fi; \
if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q & fi; \
if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q & fi; \
wait)
dev-web: check-ports infra-up ## [MID] Start Web app only (assumes backend elsewhere or mocked)
@$(ECHO_CMD) "${GREEN}[Web] Starting Vite...${NC}"
@cd $(ROOT)/$(SERVICE_DIR_web) && npm run dev
dev-backend-api: check-ports infra-up ## [MID] Start Go backend only
@$(ECHO_CMD) "${GREEN}[Backend API] Starting...${NC}"
@if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air; else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go; fi
dev-chat-server: check-ports infra-up ## [MID] Start Chat server only
@$(ECHO_CMD) "${GREEN}[Chat] Starting...${NC}"
@if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q; else cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q; fi
dev-stream-server: check-ports infra-up ## [MID] Start Stream server only
@$(ECHO_CMD) "${GREEN}[Stream] Starting...${NC}"
@if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q; else cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q; fi
stop-local-services: ## [LOW] Stop all local processes (air, cargo watch, vite)
@pkill -f "air\|cargo watch\|npm run dev\|go run.*modern-server" 2>/dev/null || true
start-local-service: ## [LOW] Start a service locally (usage: make start-local-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi
@$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}"
@case "$(SERVICE)" in \
backend-api) \
if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & fi ;; \
chat-server) \
if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q & fi ;; \
stream-server) \
if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q & fi ;; \
web) \
cd $(ROOT)/$(SERVICE_DIR_web) && npm run dev & ;; \
*) \
$(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}"; exit 1 ;; \
esac
stop-local-service: ## [LOW] Stop a local service (usage: make stop-local-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi
@case "$(SERVICE)" in \
backend-api) pkill -f "air\|go run.*modern-server" 2>/dev/null || true ;; \
chat-server|stream-server) pkill -f "cargo.*$(SERVICE)" 2>/dev/null || true ;; \
web) pkill -f "npm run dev\|vite" 2>/dev/null || true ;; \
*) $(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}" ;; \
esac

27
make/help.mk Normal file
View file

@ -0,0 +1,27 @@
# ==============================================================================
# HELP & DASHBOARD
# ==============================================================================
.PHONY: help
help: ## [HIGH] Show this dashboard
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${PURPLE}⚡ VEZA MONOREPO CLI ⚡${NC}"
@$(ECHO_CMD) "================================================================="
@$(ECHO_CMD) "${BOLD}INFRASTRUCTURE:${NC}"
@printf " ${CYAN}%-15s${NC} %s\n" "Postgres" "$(DATABASE_URL)"
@printf " ${CYAN}%-15s${NC} %s\n" "Redis" "$(REDIS_URL)"
@printf " ${CYAN}%-15s${NC} %s\n" "RabbitMQ" "UI: http://localhost:15672 (veza/password)"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${GREEN}HIGH LEVEL:${NC}"
@grep -h -E '^[a-zA-Z0-9_-]+:.*?## \[HIGH\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${YELLOW}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${BLUE}INTERMEDIATE:${NC}"
@grep -h -E '^[a-zA-Z0-9_-]+:.*?## \[MID\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${CYAN}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}${PURPLE}LOW LEVEL / DEBUG:${NC}"
@grep -h -E '^[a-zA-Z0-9_-]+:.*?## \[LOW\] .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " ${PURPLE}%-25s${NC} %s\n", $$1, $$2}'
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}PER-SERVICE (e.g. make dev-web, make test-backend-api):${NC}"
@$(ECHO_CMD) " ${CYAN}dev-<service>${NC} test-<service> lint-<service> build-<service>"
@$(ECHO_CMD) " Services: backend-api, chat-server, stream-server, web"
@$(ECHO_CMD) ""

77
make/high.mk Normal file
View file

@ -0,0 +1,77 @@
# ==============================================================================
# HIGH LEVEL: setup, stop-all, clean, deploy, status
# ==============================================================================
.PHONY: setup stop-all restart-all clean clean-deep deploy-docker deploy-incus status-full
.PHONY: web-minimal stop-minimal
setup: check-tools install-tools install-deps ## [HIGH] Full project initialization
@$(ECHO_CMD) "${BOLD}${GREEN}✅ Setup Complete! Ready to rock with 'make dev'.${NC}"
web-minimal: ## [HIGH] Start Veza Web Minimal Journey (Backend + Frontend + DB)
@$(SCRIPTS)/start_minimal.sh
stop-minimal: ## [HIGH] Stop Minimal Stack
@$(SCRIPTS)/stop_minimal.sh
stop-all: ## [HIGH] Stop all services (Docker + Local)
@$(ECHO_CMD) "${RED}🛑 Stopping all services...${NC}"
@docker compose -f $(COMPOSE_FILE) down 2>/dev/null || true
@docker compose -f $(COMPOSE_PROD) down 2>/dev/null || true
@$(MAKE) -s stop-local-services
@$(ECHO_CMD) "${GREEN}✅ All services stopped.${NC}"
restart-all: stop-all ## [HIGH] Restart all services
@$(ECHO_CMD) "${BLUE}🔄 Restarting all services...${NC}"
@$(MAKE) -s infra-up
@$(MAKE) -s dev
@$(ECHO_CMD) "${GREEN}✅ All services restarted.${NC}"
clean: ## [HIGH] Clean build artifacts and caches
@$(ECHO_CMD) "${YELLOW}🧹 Cleaning build artifacts...${NC}"
@rm -rf $(ROOT)/$(SERVICE_DIR_web)/node_modules/.cache
@rm -rf $(ROOT)/$(SERVICE_DIR_chat-server)/target/debug $(ROOT)/$(SERVICE_DIR_stream-server)/target/debug
@find $(ROOT) -type d -name "node_modules" -prune -o -type f -name "*.log" -delete 2>/dev/null || true
@$(ECHO_CMD) "${GREEN}✅ Clean complete.${NC}"
clean-deep: ## [HIGH] ⚠️ Nuclear Clean (Confirm required)
@read -p "${RED}Are you sure? This will delete ALL builds, volumes, and caches! [y/N]${NC} " ans && [ $${ans:-N} = y ]
@$(ECHO_CMD) "${RED}☢️ DESTROYING ARTIFACTS...${NC}"
@rm -rf $(ROOT)/$(SERVICE_DIR_web)/node_modules
@rm -rf $(ROOT)/$(SERVICE_DIR_chat-server)/target $(ROOT)/$(SERVICE_DIR_stream-server)/target
@docker compose -f $(COMPOSE_FILE) down -v 2>/dev/null || true
@docker compose -f $(COMPOSE_PROD) down -v 2>/dev/null || true
@$(ECHO_CMD) "${GREEN}System Cleaned.${NC}"
deploy-docker: build-all ## [HIGH] Deploy all services with Docker + HAProxy
@$(ECHO_CMD) "${BOLD}${BLUE}🐳 Deploying with Docker...${NC}"
@docker compose -f $(COMPOSE_PROD) up -d --build
@$(MAKE) -s wait-for-services
@$(ECHO_CMD) "${GREEN}✅ Deployment complete! Access via http://localhost:$(PORT_haproxy)${NC}"
deploy-incus: build-all-native ## [HIGH] Deploy all services with Incus containers (native, no Docker)
@$(ECHO_CMD) "${BOLD}${BLUE}📦 Deploying with Incus (native)...${NC}"
@$(MAKE) -s incus-setup-network
@$(MAKE) -s incus-deploy-infra
@$(MAKE) -s incus-deploy-all-native
@$(MAKE) -s incus-start-all
@$(ECHO_CMD) "${GREEN}✅ Incus deployment complete!${NC}"
@$(ECHO_CMD) "${BLUE}Access services at:${NC}"
@$(ECHO_CMD) " Backend API: http://10.10.10.2:8080"
@$(ECHO_CMD) " Chat Server: http://10.10.10.3:8081"
@$(ECHO_CMD) " Stream Server: http://10.10.10.4:3002"
@$(ECHO_CMD) " Web Frontend: http://10.10.10.5:80"
@$(ECHO_CMD) " HAProxy: http://10.10.10.6:80"
status-full: ## [HIGH] Show complete system status
@$(ECHO_CMD) "${BOLD}${CYAN}📊 SYSTEM STATUS${NC}"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Docker Containers:${NC}"
@docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "NAME|veza" || echo " No containers running"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Local Processes:${NC}"
@lsof -i :$(PORT_backend-api) -i :$(PORT_chat-server) -i :$(PORT_stream-server) -i :$(PORT_web) 2>/dev/null | grep LISTEN || echo " No local processes"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Incus Containers:${NC}"
@incus list veza- 2>/dev/null | grep -E "NAME|veza" || echo " No Incus containers"
@$(ECHO_CMD) ""

201
make/incus.mk Normal file
View file

@ -0,0 +1,201 @@
# ==============================================================================
# INCUS / LXD DEPLOYMENT
# ==============================================================================
.PHONY: incus-setup-network incus-deploy-all incus-deploy-all-native incus-deploy-service incus-deploy-service-native incus-deploy-infra incus-start-all incus-stop-all incus-status incus-logs
incus-setup-network: ## [LOW] Setup Incus network profile
@$(ECHO_CMD) "${BLUE}📦 Setting up Incus network...${NC}"
@if ! incus network show $(INCUS_NETWORK) >/dev/null 2>&1; then \
$(ECHO_CMD) "Creating network $(INCUS_NETWORK)..."; \
incus network create $(INCUS_NETWORK) \
ipv4.address=10.10.10.1/24 \
ipv4.nat=true \
ipv4.dhcp=true \
dns.mode=managed \
dns.nameservers=8.8.8.8,1.1.1.1; \
else \
$(ECHO_CMD) "Updating network configuration..."; \
incus network set $(INCUS_NETWORK) ipv4.dhcp=true 2>/dev/null || true; \
incus network set $(INCUS_NETWORK) dns.mode=managed 2>/dev/null || true; \
incus network set $(INCUS_NETWORK) dns.nameservers=8.8.8.8,1.1.1.1 2>/dev/null || true; \
fi
@if ! incus profile show $(INCUS_PROFILE) >/dev/null 2>&1; then \
$(ECHO_CMD) "Creating profile $(INCUS_PROFILE)..."; \
incus profile create $(INCUS_PROFILE); \
incus profile device add $(INCUS_PROFILE) root disk path=/ pool=default 2>/dev/null || \
incus profile device add $(INCUS_PROFILE) root disk path=/ 2>/dev/null || true; \
incus profile device add $(INCUS_PROFILE) eth0 nic network=$(INCUS_NETWORK) 2>/dev/null || true; \
else \
$(ECHO_CMD) "Ensuring profile devices..."; \
if ! incus profile show $(INCUS_PROFILE) | grep -q "root:"; then \
incus profile device add $(INCUS_PROFILE) root disk path=/ pool=default 2>/dev/null || \
incus profile device add $(INCUS_PROFILE) root disk path=/ 2>/dev/null || true; \
fi; \
if ! incus profile show $(INCUS_PROFILE) | grep -q "eth0:"; then \
incus profile device add $(INCUS_PROFILE) eth0 nic network=$(INCUS_NETWORK) 2>/dev/null || true; \
fi; \
fi
@$(ECHO_CMD) "${GREEN}✅ Incus network ready.${NC}"
incus-deploy-all: incus-setup-network ## [MID] Deploy all services to Incus (legacy Docker method)
@$(ECHO_CMD) "${BLUE}📦 Deploying all services to Incus (Docker)...${NC}"
@$(MAKE) -s incus-deploy-service SERVICE=backend-api
@$(MAKE) -s incus-deploy-service SERVICE=chat-server
@$(MAKE) -s incus-deploy-service SERVICE=stream-server
@$(MAKE) -s incus-deploy-service SERVICE=web
@$(MAKE) -s incus-deploy-service SERVICE=haproxy
@$(ECHO_CMD) "${GREEN}✅ All services deployed to Incus.${NC}"
incus-deploy-all-native: incus-setup-network ## [MID] Deploy all services to Incus (native, no Docker) - excludes Rust services
@$(ECHO_CMD) "${BLUE}📦 Deploying all services to Incus (native, excluding Rust services)...${NC}"
@$(ECHO_CMD) "${YELLOW}⚠️ Note: chat-server and stream-server are excluded${NC}"
@$(MAKE) -s incus-deploy-service-native SERVICE=backend-api
@$(MAKE) -s incus-deploy-service-native SERVICE=web
@$(MAKE) -s incus-deploy-service-native SERVICE=haproxy
@$(ECHO_CMD) "${GREEN}✅ All services deployed to Incus.${NC}"
incus-deploy-service: ## [LOW] Deploy a service to Incus with Docker (usage: make incus-deploy-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}📦 Deploying $(SERVICE) to Incus (Docker)...${NC}"
@if incus list -c n --format csv | grep -q "^veza-$(SERVICE)$$"; then \
$(ECHO_CMD) "${YELLOW}Container exists, removing...${NC}"; \
incus delete veza-$(SERVICE) --force; \
fi
@incus init images:debian/13 veza-$(SERVICE) --profile $(INCUS_PROFILE)
@incus start veza-$(SERVICE)
@$(ECHO_CMD) "${BLUE}Installing Docker in container...${NC}"
@incus exec veza-$(SERVICE) -- bash -c "apt-get update && apt-get install -y docker.io docker-compose && systemctl enable docker && systemctl start docker" || true
@$(ECHO_CMD) "${GREEN}$(SERVICE) deployed.${NC}"
incus-deploy-service-native: ## [LOW] Deploy a service to Incus natively (usage: make incus-deploy-service-native SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}📦 Deploying $(SERVICE) to Incus (native)...${NC}"
@$(INCUS_SCRIPTS)/deploy-service-native.sh $(SERVICE)
incus-deploy-infra: incus-setup-network ## [LOW] Deploy infrastructure services (PostgreSQL, Redis)
@$(ECHO_CMD) "${BLUE}📦 Deploying infrastructure services...${NC}"
@$(MAKE) -s incus-deploy-service-native SERVICE=infra
@$(ECHO_CMD) "${BLUE}Waiting for infrastructure to be ready...${NC}"
@for i in $$(seq 1 30); do \
if incus exec veza-infra -- systemctl is-active postgresql >/dev/null 2>&1 && \
incus exec veza-infra -- systemctl is-active redis-server >/dev/null 2>&1; then \
$(ECHO_CMD) "${GREEN}✅ Infrastructure services ready${NC}"; \
break; \
fi; \
sleep 1; \
done
@$(ECHO_CMD) "${GREEN}✅ Infrastructure deployed.${NC}"
incus-start-all: ## [MID] Start all Incus services (excluding Rust services)
@$(ECHO_CMD) "${BLUE}🚀 Starting all Incus services (excluding Rust services)...${NC}"
@for service in backend-api; do \
if incus list -c n --format csv | grep -q "^veza-$$service$$"; then \
$(ECHO_CMD) "Starting veza-$$service..."; \
if incus exec veza-$$service -- systemctl start veza-$$service 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ veza-$$service started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ veza-$$service failed to start (check logs)${NC}"; \
fi; \
fi; \
done
@if incus list -c n --format csv | grep -q "^veza-web$$"; then \
$(ECHO_CMD) "Starting veza-web..."; \
if incus exec veza-web -- systemctl start apache2 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ Apache started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ Apache failed to start${NC}"; \
fi; \
fi
@if incus list -c n --format csv | grep -q "^veza-haproxy$$"; then \
$(ECHO_CMD) "Starting veza-haproxy..."; \
if incus exec veza-haproxy -- systemctl start haproxy 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ HAProxy started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ HAProxy failed to start${NC}"; \
fi; \
fi
@if incus list -c n --format csv | grep -q "^veza-infra$$"; then \
$(ECHO_CMD) "Starting infrastructure services..."; \
if incus exec veza-infra -- systemctl start postgresql 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ PostgreSQL started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ PostgreSQL failed to start${NC}"; \
fi; \
if incus exec veza-infra -- systemctl start redis-server 2>/dev/null; then \
$(ECHO_CMD) "${GREEN} ✅ Redis started${NC}"; \
else \
$(ECHO_CMD) "${YELLOW} ⚠️ Redis failed to start${NC}"; \
fi; \
fi
@$(ECHO_CMD) "${GREEN}✅ All services started.${NC}"
@$(ECHO_CMD) "${BLUE}Run 'make incus-status' to check service status${NC}"
incus-stop-all: ## [MID] Stop all Incus containers
@$(ECHO_CMD) "${YELLOW}🛑 Stopping all Incus containers...${NC}"
@for container in $$(incus list -c n --format csv | grep veza-); do \
incus stop $$container 2>/dev/null || true; \
done
@$(ECHO_CMD) "${GREEN}✅ All Incus containers stopped.${NC}"
incus-status: ## [MID] Show status of all Incus services
@$(ECHO_CMD) "${BOLD}${CYAN}📊 INCUS DEPLOYMENT STATUS${NC}"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Containers:${NC}"
@incus list veza- --format table 2>/dev/null || echo " No containers found"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}Service Status:${NC}"
@for service in backend-api chat-server stream-server; do \
if incus list -c n --format csv 2>/dev/null | grep -q "^veza-$$service$$"; then \
STATUS=$$(incus exec veza-$$service -- systemctl is-active veza-$$service 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-$$service: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-$$service: $$STATUS${NC}"; \
fi; \
fi; \
done
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-web$$"; then \
STATUS=$$(incus exec veza-web -- systemctl is-active apache2 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-web (Apache): active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-web (Apache): $$STATUS${NC}"; \
fi; \
fi
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-haproxy$$"; then \
STATUS=$$(incus exec veza-haproxy -- systemctl is-active haproxy 2>/dev/null || echo "inactive"); \
if [ "$$STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ veza-haproxy: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ veza-haproxy: $$STATUS${NC}"; \
fi; \
fi
@if incus list -c n --format csv 2>/dev/null | grep -q "^veza-infra$$"; then \
PG_STATUS=$$(incus exec veza-infra -- systemctl is-active postgresql 2>/dev/null || echo "inactive"); \
REDIS_STATUS=$$(incus exec veza-infra -- systemctl is-active redis-server 2>/dev/null || echo "inactive"); \
if [ "$$PG_STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ PostgreSQL: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ PostgreSQL: $$PG_STATUS${NC}"; \
fi; \
if [ "$$REDIS_STATUS" = "active" ]; then \
$(ECHO_CMD) " ${GREEN}✅ Redis: active${NC}"; \
else \
$(ECHO_CMD) " ${YELLOW}⚠️ Redis: $$REDIS_STATUS${NC}"; \
fi; \
fi
@$(ECHO_CMD) ""
incus-logs: ## [LOW] Show logs from Incus container (usage: make incus-logs SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@incus exec veza-$(SERVICE) -- journalctl -f

45
make/infra.mk Normal file
View file

@ -0,0 +1,45 @@
# ==============================================================================
# INFRASTRUCTURE (Docker: Postgres, Redis, RabbitMQ)
# ==============================================================================
.PHONY: infra-up infra-down wait-for-infra wait-for-services db-shell redis-shell db-migrate
infra-up: ## [MID] Start Docker Infra (with health checks)
@$(ECHO_CMD) "${BLUE}🐳 Starting Infrastructure...${NC}"
@docker compose -f $(COMPOSE_FILE) up -d
@$(MAKE) -s wait-for-infra
infra-down: ## [MID] Stop Docker Infra
@$(ECHO_CMD) "${BLUE}🛑 Stopping Infrastructure...${NC}"
@docker compose -f $(COMPOSE_FILE) down
wait-for-infra: ## [LOW] Wait for infrastructure to be ready
@printf "${BLUE}⏳ Waiting for services...${NC}"
@until docker compose -f $(COMPOSE_FILE) exec -T postgres pg_isready -U $(DB_USER) > /dev/null 2>&1; do printf "."; sleep 1; done
@until docker compose -f $(COMPOSE_FILE) exec -T redis redis-cli ping > /dev/null 2>&1; do printf "."; sleep 1; done
@$(ECHO_CMD) " ${GREEN}OK${NC}"
wait-for-services: ## [LOW] Wait for all application services
@printf "${BLUE}⏳ Waiting for services...${NC}"
@for service in backend-api chat-server stream-server web; do \
until docker compose -f $(COMPOSE_PROD) exec -T $$service echo "ready" > /dev/null 2>&1; do \
printf "."; sleep 1; \
done; \
done
@$(ECHO_CMD) " ${GREEN}OK${NC}"
db-shell: ## [MID] Connect to Postgres shell
@docker compose -f $(COMPOSE_FILE) exec postgres psql -U $(DB_USER) -d $(DB_NAME)
redis-shell: ## [MID] Connect to Redis shell
@docker compose -f $(COMPOSE_FILE) exec redis redis-cli
db-migrate: infra-up ## [MID] Run all database migrations
@$(ECHO_CMD) "${BLUE}🔄 Running Migrations...${NC}"
@$(ECHO_CMD) " -> [Go] Migrating..."
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/migrate_tool/main.go up || $(ECHO_CMD) "${YELLOW}Warning: Go migration failed${NC}")
@$(ECHO_CMD) " -> [Chat] Migrating..."
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && sqlx migrate run || $(ECHO_CMD) "${YELLOW}Warning: Chat migration failed${NC}")
@$(ECHO_CMD) " -> [Stream] Migrating..."
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && sqlx migrate run || $(ECHO_CMD) "${YELLOW}Warning: Stream migration failed${NC}")
@$(ECHO_CMD) "${GREEN}✅ Migrations done.${NC}"

38
make/services.mk Normal file
View file

@ -0,0 +1,38 @@
# ==============================================================================
# SERVICE LIFECYCLE (Docker: start/stop/restart/logs per service)
# ==============================================================================
.PHONY: start-service stop-service restart-service logs-service
start-service: ## [MID] Start a specific service (usage: make start-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}"
@docker compose -f $(COMPOSE_PROD) up -d $(SERVICE) 2>/dev/null || \
$(MAKE) -s start-local-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) started.${NC}"
stop-service: ## [MID] Stop a specific service (usage: make stop-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@$(ECHO_CMD) "${YELLOW}🛑 Stopping $(SERVICE)...${NC}"
@docker compose -f $(COMPOSE_PROD) stop $(SERVICE) 2>/dev/null || \
$(MAKE) -s stop-local-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) stopped.${NC}"
restart-service: stop-service ## [MID] Restart a specific service (usage: make restart-service SERVICE=backend-api)
@$(ECHO_CMD) "${BLUE}🔄 Restarting $(SERVICE)...${NC}"
@$(MAKE) -s start-service SERVICE=$(SERVICE)
@$(ECHO_CMD) "${GREEN}$(SERVICE) restarted.${NC}"
logs-service: ## [MID] Show logs for a service (usage: make logs-service SERVICE=backend-api)
@if [ -z "$(SERVICE)" ]; then \
$(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; \
exit 1; \
fi
@docker compose -f $(COMPOSE_PROD) logs -f $(SERVICE) 2>/dev/null || \
$(ECHO_CMD) "${YELLOW}Service not running in Docker, check local logs${NC}"

71
make/test.mk Normal file
View file

@ -0,0 +1,71 @@
# ==============================================================================
# TEST & QUALITY (unit tests, lint, format)
# ==============================================================================
.PHONY: test test-tmt lint fmt status test-web test-backend-api test-chat-server test-stream-server
.PHONY: lint-web lint-backend-api lint-chat-server lint-stream-server
test: infra-up ## [MID] Run All Tests (Fastest strategy)
@$(ECHO_CMD) "${BLUE}🧪 Running Tests...${NC}"
@$(ECHO_CMD) " [Go] Unit Tests..."
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && go test ./... -short)
@$(ECHO_CMD) " [Rust] Unit Tests..."
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo test --lib -q)
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo test --lib -q)
@$(ECHO_CMD) " [Web] Unit Tests..."
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm run test -- --run)
@$(ECHO_CMD) "${GREEN}✅ All tests passed.${NC}"
test-tmt: ## [MID] Run Unified TMT Pipeline
@$(ECHO_CMD) "${BLUE}🧪 Running TMT Pipeline...${NC}"
@command -v tmt >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ tmt is missing! Install with 'pip install tmt'${NC}"; exit 1; }
@tmt run
test-web: ## [MID] Run Web tests only
@$(ECHO_CMD) "${BLUE}🧪 Running Web tests...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm run test -- --run)
test-backend-api: ## [MID] Run Go backend tests only
@$(ECHO_CMD) "${BLUE}🧪 Running Backend API tests...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && go test ./... -short)
test-chat-server: ## [MID] Run Chat server tests only
@$(ECHO_CMD) "${BLUE}🧪 Running Chat server tests...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo test --lib -q)
test-stream-server: ## [MID] Run Stream server tests only
@$(ECHO_CMD) "${BLUE}🧪 Running Stream server tests...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo test --lib -q)
lint: ## [MID] Lint everything
@$(ECHO_CMD) "${BLUE}🔍 Linting Codebase...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo clippy -- -D warnings) || true
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo clippy -- -D warnings) || true
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && golangci-lint run ./...) || true
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm run lint) || true
lint-web: ## [MID] Lint web app only
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm run lint)
lint-backend-api: ## [MID] Lint Go backend only
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && golangci-lint run ./...)
lint-chat-server: ## [MID] Lint Chat server only
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo clippy -- -D warnings)
lint-stream-server: ## [MID] Lint Stream server only
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo clippy -- -D warnings)
fmt: ## [MID] Format everything
@$(ECHO_CMD) "${BLUE}✨ Formatting...${NC}"
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && go fmt ./...)
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo fmt)
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo fmt)
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm run format) || true
status: ## [MID] Show system health & stats
@$(ECHO_CMD) "${BOLD}DOCKER STATS:${NC}"
@docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" 2>/dev/null | grep -E "NAME|veza" || echo "No containers running"
@$(ECHO_CMD) ""
@$(ECHO_CMD) "${BOLD}LOCAL PORTS:${NC}"
@lsof -i :$(PORT_backend-api) -i :$(PORT_chat-server) -i :$(PORT_stream-server) -i :$(PORT_web) 2>/dev/null | grep LISTEN || echo "No apps listening."

49
make/tools.mk Normal file
View file

@ -0,0 +1,49 @@
# ==============================================================================
# TOOLS: check, install deps, ports
# ==============================================================================
.PHONY: check-tools check-tools-incus install-tools install-deps check-ports
check-tools: ## [LOW] Check required tools
@$(ECHO_CMD) "${BLUE}Checking core requirements...${NC}"
@for tool in docker go cargo npm; do \
command -v $$tool >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}$$tool is missing!${NC}"; exit 1; }; \
done
@$(ECHO_CMD) "${GREEN}✅ All tools present.${NC}"
check-tools-incus: ## [LOW] Check required tools for Incus deployment
@$(ECHO_CMD) "${BLUE}Checking Incus deployment requirements...${NC}"
@command -v incus >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ incus is missing! Install with: sudo snap install incus${NC}"; exit 1; }
@command -v go >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ go is missing!${NC}"; exit 1; }
@command -v cargo >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ cargo is missing!${NC}"; exit 1; }
@command -v npm >/dev/null 2>&1 || { $(ECHO_CMD) "${RED}❌ npm is missing!${NC}"; exit 1; }
@$(ECHO_CMD) "${GREEN}✅ All Incus tools present.${NC}"
install-tools: ## [LOW] Install Power User tools (Hot Reload, Linters)
@$(ECHO_CMD) "${BLUE}🛠️ Installing Dev Tools...${NC}"
@command -v air >/dev/null 2>&1 || go install github.com/air-verse/air@latest
@command -v cargo-watch >/dev/null 2>&1 || cargo install cargo-watch
@command -v sqlx >/dev/null 2>&1 || cargo install sqlx-cli --no-default-features --features native-tls,postgres
@$(ECHO_CMD) "${GREEN}✅ Tools installed.${NC}"
install-deps: ## [LOW] Install code dependencies (all backends + npm workspaces)
@$(ECHO_CMD) "${BLUE}📦 Installing dependencies...${NC}"
@$(ECHO_CMD) " -> [Go] Downloading modules..."
@(cd $(ROOT)/$(SERVICE_DIR_backend-api) && go mod download)
@$(ECHO_CMD) " -> [Rust Chat] Fetching crates..."
@(cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo fetch)
@$(ECHO_CMD) " -> [Rust Stream] Fetching crates..."
@(cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo fetch)
@$(ECHO_CMD) " -> [Web] Installing npm packages..."
@(cd $(ROOT)/$(SERVICE_DIR_web) && npm install --silent)
@$(ECHO_CMD) "${GREEN}✅ Dependencies installed.${NC}"
check-ports: ## [LOW] Check if ports are available
@$(ECHO_CMD) "${BLUE}🔍 Checking ports...${NC}"
@for port in $(PORT_backend-api) $(PORT_chat-server) $(PORT_stream-server) $(PORT_web); do \
if lsof -i :$$port -t >/dev/null 2>&1; then \
$(ECHO_CMD) "${YELLOW}⚠️ Port $$port is busy${NC}"; \
else \
$(ECHO_CMD) "${GREEN}✅ Port $$port is free${NC}"; \
fi; \
done

14
make/ui.mk Normal file
View file

@ -0,0 +1,14 @@
# ==============================================================================
# UI: colors and echo helper
# ==============================================================================
BOLD := \033[1m
RED := \033[0;31m
GREEN := \033[0;32m
YELLOW := \033[0;33m
BLUE := \033[0;34m
PURPLE := \033[0;35m
CYAN := \033[0;36m
NC := \033[0m
ECHO_CMD := echo -e

View file

@ -0,0 +1,72 @@
# Account lockout (BE-SEC-007)
## How it works
After too many **failed login attempts**, the account is temporarily **locked** to slow down brute-force attacks.
1. **Storage**: Lock state and attempt counts are stored in **Redis** (keys `account_lockout:attempts:{email}` and `account_lockout:locked:{email}`).
2. **Failed attempt**: Each failed login (wrong password, user not found, or email not verified) calls `RecordFailedAttempt(email)`. The attempt counter is incremented; it expires after a **window** (default 15 minutes).
3. **Lock**: When the number of failed attempts in the window reaches **max attempts** (default 5), the account is **locked** for a **lockout duration** (default 30 minutes).
4. **While locked**: Any login for that email returns **HTTP 423 Locked** with message *"Account is locked. Please try again later."*
5. **Unlock**: The lock key has a TTL; when it expires, the account is automatically unlocked. A **successful login** also clears the lock and the attempt counter.
**Defaults** (see `internal/services/account_lockout_service.go`):
- Max attempts: **5**
- Window: **15 minutes**
- Lockout duration: **30 minutes**
If Redis is unavailable, lockout is disabled (no locking, no recording).
---
## Unlock an account
### Option 1: Admin API (recommended)
As an **admin** user, send:
```bash
curl -X POST http://localhost:8080/api/v1/admin/auth/unlock-account \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <ADMIN_ACCESS_TOKEN>" \
-d '{"email":"user@example.com"}'
```
Response: `200 OK` with `{"message":"account unlocked","email":"user@example.com"}`.
### Option 2: Redis CLI
If you have access to Redis:
```bash
# Replace with the locked user's email
EMAIL="user@example.com"
redis-cli DEL "account_lockout:locked:${EMAIL}" "account_lockout:attempts:${EMAIL}"
```
---
## Disable lockout for specific accounts
Use **exempt emails** so those accounts are never locked and failed attempts are not recorded.
**Environment variable** (comma-separated list):
```bash
ACCOUNT_LOCKOUT_EXEMPT_EMAILS=testuser@example.com,admin@test.com
```
Example in `.env` or `.env.development`:
```
ACCOUNT_LOCKOUT_EXEMPT_EMAILS=testuser@example.com
```
After restarting the API, that email will:
- Never be considered locked (`IsAccountLocked` returns false).
- Not have failed attempts recorded (`RecordFailedAttempt` is a no-op).
This is intended for **test / dev accounts** only; avoid exempting real user emails in production.

View file

@ -48,6 +48,7 @@ type APIRouter struct {
logger *zap.Logger logger *zap.Logger
versionManager *VersionManager // BE-SVC-019: API versioning manager versionManager *VersionManager // BE-SVC-019: API versioning manager
monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting
authService *authcore.AuthService // Set by setupAuthRoutes for admin unlock
} }
// NewAPIRouter crée une nouvelle instance de APIRouter // NewAPIRouter crée une nouvelle instance de APIRouter
@ -280,12 +281,12 @@ func (r *APIRouter) Setup(router *gin.Engine) error {
// Groupe API v1 (nouveau frontend React) // Groupe API v1 (nouveau frontend React)
v1 := router.Group("/api/v1") v1 := router.Group("/api/v1")
{ {
// Routes core protégées (sessions, uploads, audit, admin, conversations) // Auth routes first so r.authService is set for admin unlock in setupCoreProtectedRoutes
r.setupCoreProtectedRoutes(v1)
if err := r.setupAuthRoutes(v1); err != nil { if err := r.setupAuthRoutes(v1); err != nil {
return err return err
} }
// Routes core protégées (sessions, uploads, audit, admin, conversations)
r.setupCoreProtectedRoutes(v1)
// Action 5.2.1.1: Validation endpoint for pre-validation // Action 5.2.1.1: Validation endpoint for pre-validation
r.setupValidateRoutes(v1) r.setupValidateRoutes(v1)
@ -407,11 +408,18 @@ func (r *APIRouter) setupAuthRoutes(router *gin.RouterGroup) error {
// BE-SEC-007: Initialize account lockout service and set it on auth service // BE-SEC-007: Initialize account lockout service and set it on auth service
if r.config.RedisClient != nil { if r.config.RedisClient != nil {
accountLockoutService := services.NewAccountLockoutService(r.config.RedisClient, r.logger) lockoutConfig := &services.AccountLockoutConfig{
MaxAttempts: 5,
LockoutDuration: 30 * time.Minute,
WindowDuration: 15 * time.Minute,
ExemptEmails: r.config.AccountLockoutExemptEmails,
}
accountLockoutService := services.NewAccountLockoutServiceWithConfig(r.config.RedisClient, r.logger, lockoutConfig)
authService.SetAccountLockoutService(accountLockoutService) authService.SetAccountLockoutService(accountLockoutService)
} else { } else {
r.logger.Warn("Redis not available - account lockout disabled") r.logger.Warn("Redis not available - account lockout disabled")
} }
r.authService = authService
// 2.5. User Service for GetMe endpoint // 2.5. User Service for GetMe endpoint
userRepo := repositories.NewGormUserRepository(r.db.GormDB) userRepo := repositories.NewGormUserRepository(r.db.GormDB)
@ -1421,6 +1429,11 @@ func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) {
// MOD-P2-006: Profiling pprof (protégé par auth admin) // MOD-P2-006: Profiling pprof (protégé par auth admin)
admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux)) admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux))
// BE-SEC-007: Unlock account locked by failed login attempts (admin only)
if r.authService != nil {
admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger))
}
} }
} }

Some files were not shown because too many files have changed in this diff Show more