base structure
This commit is contained in:
parent
50fe5ef848
commit
b93c739e35
31 changed files with 735 additions and 0 deletions
8
.env
Normal file
8
.env
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Variables para los servicios PostgreSQL y Redis en Docker Compose
|
||||
# El backend lee sus propias variables desde backend/.env
|
||||
|
||||
POSTGRES_DB=wearlab_dev
|
||||
POSTGRES_USER=app
|
||||
POSTGRES_PASSWORD=password
|
||||
|
||||
REDIS_PASSWORD=
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# ── Secrets ───────────────────────────────────────────────────────────────────
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# ── PHP / Symfony ─────────────────────────────────────────────────────────────
|
||||
backend/vendor/
|
||||
backend/var/cache/
|
||||
backend/var/log/
|
||||
backend/var/sessions/
|
||||
backend/.php-cs-fixer.cache
|
||||
backend/.phpunit.cache/
|
||||
|
||||
# ── Node / Frontend ───────────────────────────────────────────────────────────
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
frontend/coverage/
|
||||
|
||||
# ── OS & editor ───────────────────────────────────────────────────────────────
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*~
|
||||
8
backend/.env
Normal file
8
backend/.env
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
APP_ENV=dev
|
||||
APP_DEBUG=1
|
||||
APP_SECRET=changeme-generate-a-32-char-random-string
|
||||
|
||||
DATABASE_URL="postgresql://app:password@postgres:5432/wearlab_dev?serverVersion=16&charset=utf8"
|
||||
REDIS_URL="redis://redis:6379"
|
||||
|
||||
CORS_ALLOW_ORIGIN=http://localhost:8080
|
||||
64
backend/Dockerfile
Normal file
64
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# ── Stage: base ──────────────────────────────────────────────────────────────
|
||||
FROM php:8.4-fpm-alpine AS base
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libpq-dev \
|
||||
icu-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
git \
|
||||
curl \
|
||||
autoconf \
|
||||
g++ \
|
||||
make
|
||||
|
||||
RUN docker-php-ext-install \
|
||||
pdo_pgsql \
|
||||
intl \
|
||||
opcache \
|
||||
zip
|
||||
|
||||
RUN pecl channel-update pecl.php.net && pecl install apcu redis && docker-php-ext-enable apcu redis
|
||||
|
||||
RUN echo "clear_env = no" >> /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ── Stage: dev ───────────────────────────────────────────────────────────────
|
||||
FROM base AS dev
|
||||
|
||||
RUN apk add --no-cache linux-headers
|
||||
|
||||
RUN pecl channel-update pecl.php.net && pecl install xdebug && docker-php-ext-enable xdebug
|
||||
RUN printf "xdebug.mode=debug\nxdebug.start_with_request=yes\nxdebug.client_port=9003\nxdebug.log_level=0\n" \
|
||||
> /usr/local/etc/php/conf.d/xdebug.ini
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["php-fpm"]
|
||||
|
||||
# ── Stage: prod ──────────────────────────────────────────────────────────────
|
||||
FROM base AS prod
|
||||
|
||||
RUN printf "opcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n" \
|
||||
> /usr/local/etc/php/conf.d/opcache-prod.ini
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN composer install \
|
||||
--no-dev \
|
||||
--no-interaction \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader \
|
||||
--no-scripts
|
||||
|
||||
RUN chmod +x bin/console
|
||||
|
||||
EXPOSE 9000
|
||||
CMD ["php-fpm"]
|
||||
16
backend/bin/console
Normal file
16
backend/bin/console
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (class_exists(Dotenv::class)) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', (bool) ($_SERVER['APP_DEBUG'] ?? true));
|
||||
$application = new Application($kernel);
|
||||
$application->run();
|
||||
46
backend/composer.json
Normal file
46
backend/composer.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "app/wearlab-backend",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/doctrine-bundle": "^2.13",
|
||||
"doctrine/orm": "^3.3",
|
||||
"nelmio/cors-bundle": "^2.5",
|
||||
"symfony/console": "7.*",
|
||||
"symfony/dotenv": "7.*",
|
||||
"symfony/framework-bundle": "7.*",
|
||||
"symfony/var-exporter": "7.*",
|
||||
"symfony/yaml": "7.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11",
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
},
|
||||
"sort-packages": true,
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
backend/config/bundles.php
Normal file
7
backend/config/bundles.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
];
|
||||
7
backend/config/packages/doctrine.yaml
Normal file
7
backend/config/packages/doctrine.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: false
|
||||
4
backend/config/packages/framework.yaml
Normal file
4
backend/config/packages/framework.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
router:
|
||||
utf8: true
|
||||
9
backend/config/packages/nelmio_cors.yaml
Normal file
9
backend/config/packages/nelmio_cors.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
nelmio_cors:
|
||||
defaults:
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
max_age: 3600
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
5
backend/config/routes.yaml
Normal file
5
backend/config/routes.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
11
backend/config/services.yaml
Normal file
11
backend/config/services.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: false
|
||||
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Domain/Entity/'
|
||||
13
backend/docker-entrypoint.sh
Normal file
13
backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ ! -f /app/vendor/autoload.php ]; then
|
||||
echo "→ Instalando dependencias de Composer..."
|
||||
composer install --no-interaction --prefer-dist --no-scripts
|
||||
echo "→ Dependencias instaladas."
|
||||
fi
|
||||
|
||||
mkdir -p /app/var/cache /app/var/log /app/var/sessions
|
||||
chmod -R 777 /app/var
|
||||
|
||||
exec "$@"
|
||||
17
backend/public/index.php
Normal file
17
backend/public/index.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (class_exists(Dotenv::class)) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', (bool) ($_SERVER['APP_DEBUG'] ?? true));
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
||||
43
backend/src/Controller/HealthController.php
Normal file
43
backend/src/Controller/HealthController.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class HealthController
|
||||
{
|
||||
#[Route('/api/health', name: 'api_health', methods: ['GET'])]
|
||||
public function __invoke(Connection $connection): JsonResponse
|
||||
{
|
||||
try {
|
||||
$connection->executeQuery('SELECT 1');
|
||||
$dbStatus = 'ok';
|
||||
} catch (\Throwable) {
|
||||
$dbStatus = 'error';
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = new \Redis();
|
||||
$redisUrl = $_ENV['REDIS_URL'] ?? 'redis://redis:6379';
|
||||
$parsed = parse_url($redisUrl);
|
||||
$host = $parsed['host'] ?? 'redis';
|
||||
$port = (int) ($parsed['port'] ?? 6379);
|
||||
$redis->connect($host, $port, 1.0);
|
||||
$redisStatus = $redis->ping() ? 'ok' : 'error';
|
||||
$redis->close();
|
||||
} catch (\Throwable) {
|
||||
$redisStatus = 'error';
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'status' => 'ok',
|
||||
'database' => $dbStatus,
|
||||
'redis' => $redisStatus,
|
||||
'timestamp' => (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
backend/src/Kernel.php
Normal file
11
backend/src/Kernel.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
}
|
||||
0
backend/var/.gitkeep
Normal file
0
backend/var/.gitkeep
Normal file
98
docker-compose.yml
Normal file
98
docker-compose.yml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
name: wearlab
|
||||
|
||||
services:
|
||||
|
||||
# ── Frontend: Vite dev server con Hot Module Replacement ─────────────────────
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
target: dev
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- frontend_node_modules:/app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
networks:
|
||||
- app
|
||||
|
||||
# ── Backend: PHP 8.4-FPM (Symfony) ───────────────────────────────────────────
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
target: dev
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- backend_vendor:/app/vendor
|
||||
- backend_var:/app/var
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app
|
||||
|
||||
# ── Nginx: reverse proxy ──────────────────────────────────────────────────────
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./docker/nginx/nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
networks:
|
||||
- app
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost/api/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# ── PostgreSQL 16 ─────────────────────────────────────────────────────────────
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- app
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# ── Redis 7 ───────────────────────────────────────────────────────────────────
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- app
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
networks:
|
||||
app:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
frontend_node_modules:
|
||||
backend_vendor:
|
||||
backend_var:
|
||||
23
docker/nginx/nginx.dev.conf
Normal file
23
docker/nginx/nginx.dev.conf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# /api/* → Symfony vía PHP-FPM
|
||||
location /api/ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass backend:9000;
|
||||
fastcgi_param SCRIPT_FILENAME /app/public/index.php;
|
||||
fastcgi_param SCRIPT_NAME /index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /app/public;
|
||||
}
|
||||
|
||||
# Todo lo demás → Vite dev server (incluye WebSocket para HMR)
|
||||
location / {
|
||||
proxy_pass http://frontend:5173;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
39
docker/nginx/nginx.prod.conf
Normal file
39
docker/nginx/nginx.prod.conf
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# /api/* → PHP-FPM
|
||||
location /api/ {
|
||||
fastcgi_pass backend:9000;
|
||||
fastcgi_param SCRIPT_FILENAME /app/public/index.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# React SPA — todo cae en index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
location ~* \.(js|css|png|jpg|svg|woff2?)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
103
docs/ARCHITECTURE.md
Normal file
103
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Arquitectura del proyecto
|
||||
|
||||
## Estructura del repositorio
|
||||
|
||||
```
|
||||
wearlab/
|
||||
├── frontend/ ← SPA React + Vite + TypeScript
|
||||
│ ├── Dockerfile ← multi-stage: dev / build / prod
|
||||
│ ├── public/
|
||||
│ └── src/
|
||||
│ ├── components/ ← componentes UI reutilizables
|
||||
│ ├── features/ ← módulos por funcionalidad de negocio
|
||||
│ ├── hooks/ ← hooks globales
|
||||
│ ├── lib/ ← cliente HTTP, utilidades
|
||||
│ ├── pages/ ← componentes de nivel de ruta
|
||||
│ ├── router/ ← configuración de React Router
|
||||
│ ├── store/ ← estado global UI
|
||||
│ ├── styles/ ← CSS global
|
||||
│ └── types/ ← tipos TypeScript compartidos
|
||||
│
|
||||
├── backend/ ← API REST Symfony 7 + PHP 8.4
|
||||
│ ├── Dockerfile ← multi-stage: dev / prod
|
||||
│ ├── bin/console
|
||||
│ ├── config/
|
||||
│ │ └── packages/
|
||||
│ ├── migrations/
|
||||
│ ├── public/index.php
|
||||
│ └── src/
|
||||
│ ├── Controller/
|
||||
│ ├── Application/
|
||||
│ ├── Domain/
|
||||
│ ├── Infrastructure/
|
||||
│ └── Shared/
|
||||
│
|
||||
├── docker/
|
||||
│ └── nginx/
|
||||
│ ├── nginx.dev.conf ← proxy dev: /api → php-fpm, /* → Vite HMR
|
||||
│ └── nginx.prod.conf ← producción: HTTPS, SPA fallback
|
||||
│
|
||||
├── docs/
|
||||
│ └── ARCHITECTURE.md
|
||||
│
|
||||
├── docker-compose.yml ← desarrollo local
|
||||
├── .env
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cómo fluye una petición HTTP
|
||||
|
||||
### En desarrollo
|
||||
|
||||
```
|
||||
Browser
|
||||
→ http://localhost:8080 (Nginx puerto 80)
|
||||
├── /api/* → FastCGI → backend:9000 (PHP-FPM) → PostgreSQL / Redis
|
||||
└── /* → proxy → frontend:5173 (Vite HMR con WebSocket)
|
||||
```
|
||||
|
||||
### En producción
|
||||
|
||||
```
|
||||
Browser
|
||||
→ https://tudominio.com (Nginx puerto 443, TLS)
|
||||
├── /api/* → FastCGI → backend:9000 (PHP-FPM) → PostgreSQL / Redis
|
||||
└── /* → archivos estáticos compilados por Vite (/dist)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Servicios Docker
|
||||
|
||||
| Servicio | Imagen / Build | Puerto host | Descripción |
|
||||
|---|---|---|---|
|
||||
| frontend | `./frontend` (target: dev) | 5173 | Vite dev server con HMR |
|
||||
| backend | `./backend` (target: dev) | — | PHP-FPM Symfony |
|
||||
| nginx | `nginx:1.27-alpine` | 8080 | Reverse proxy |
|
||||
| postgres | `postgres:16-alpine` | 5432 | Base de datos |
|
||||
| redis | `redis:7-alpine` | 6379 | Caché y sesiones |
|
||||
|
||||
---
|
||||
|
||||
## Secretos y variables de entorno
|
||||
|
||||
| Dónde | Qué contiene | ¿Se commitea? |
|
||||
|---|---|---|
|
||||
| `.env` (repo raíz) | PostgreSQL, Redis | Sí |
|
||||
| `backend/.env` | Symfony, DATABASE_URL, REDIS_URL | Sí (valores dev) |
|
||||
| `.env.local` | Override local del desarrollador | No (.gitignore) |
|
||||
|
||||
---
|
||||
|
||||
## Branches (recomendado)
|
||||
|
||||
```
|
||||
main ── producción ── protegido ── requiere PR + CI verde
|
||||
develop ── staging ── protegido ── requiere PR + CI verde
|
||||
feature/* ── desde develop, merged via PR
|
||||
fix/* ── desde develop, merged via PR
|
||||
hotfix/* ── desde main, merged a main + develop
|
||||
```
|
||||
23
frontend/Dockerfile
Normal file
23
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# ── Stage: base ──────────────────────────────────────────────────────────────
|
||||
FROM node:22-alpine AS base
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
# ── Stage: dev ───────────────────────────────────────────────────────────────
|
||||
FROM base AS dev
|
||||
EXPOSE 5173
|
||||
CMD ["sh", "-c", "npm install && npm run dev"]
|
||||
|
||||
# ── Stage: build ─────────────────────────────────────────────────────────────
|
||||
FROM base AS build
|
||||
COPY . .
|
||||
ARG VITE_API_BASE_URL
|
||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage: prod ──────────────────────────────────────────────────────────────
|
||||
FROM nginx:1.27-alpine AS prod
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WearLab</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "wearlab-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
55
frontend/src/App.tsx
Normal file
55
frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface HealthResponse {
|
||||
status: string
|
||||
database: string
|
||||
redis: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [health, setHealth] = useState<HealthResponse | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/health')
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json() as Promise<HealthResponse>
|
||||
})
|
||||
.then((data) => setHealth(data))
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 480, margin: '4rem auto', padding: '0 1rem' }}>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '0.25rem' }}>WearLab</h1>
|
||||
<p style={{ color: '#666', marginTop: 0 }}>React + Symfony + PostgreSQL + Redis</p>
|
||||
|
||||
<hr style={{ margin: '1.5rem 0' }} />
|
||||
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '0.75rem' }}>API Health Check</h2>
|
||||
|
||||
{loading && <p style={{ color: '#888' }}>Connecting to API…</p>}
|
||||
|
||||
{error && (
|
||||
<div style={{ background: '#fee2e2', border: '1px solid #fca5a5', borderRadius: 6, padding: '0.75rem 1rem' }}>
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{health && (
|
||||
<div style={{ background: '#f0fdf4', border: '1px solid #86efac', borderRadius: 6, padding: '0.75rem 1rem' }}>
|
||||
<div>Status: <strong>{health.status}</strong></div>
|
||||
<div>Database: <strong>{health.database}</strong></div>
|
||||
<div>Redis: <strong>{health.redis}</strong></div>
|
||||
<div style={{ color: '#555', fontSize: '0.85rem', marginTop: '0.5rem' }}>{health.timestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
9
frontend/src/main.tsx
Normal file
9
frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
20
frontend/tsconfig.app.json
Normal file
20
frontend/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
15
frontend/tsconfig.node.json
Normal file
15
frontend/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
},
|
||||
})
|
||||
Loading…
Reference in a new issue