SuiteCoffee/README_tmp.html

665 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<title>README.md</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<style>
/* https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/media/markdown.css */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
body {
font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif);
font-size: var(--vscode-markdown-font-size, 14px);
padding: 0 26px;
line-height: var(--vscode-markdown-line-height, 22px);
word-wrap: break-word;
}
#code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
#code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
body.scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
body.showEditorSelection .code-line {
position: relative;
}
body.showEditorSelection .code-active-line:before,
body.showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
body.showEditorSelection li.code-active-line:before,
body.showEditorSelection li.code-line:hover:before {
left: -30px;
}
.vscode-light.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
.vscode-light.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
.vscode-light.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-dark.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vscode-dark.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vscode-dark.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-high-contrast.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.vscode-high-contrast.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
.vscode-high-contrast.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
h1, h2, h3 {
font-weight: normal;
}
table {
border-collapse: collapse;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 1em;
line-height: 1.357em;
}
body.wordWrap pre {
white-space: pre-wrap;
}
pre:not(.hljs),
pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
pre code {
color: var(--vscode-editor-foreground);
tab-size: 4;
}
/** Theming */
.vscode-light pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vscode-dark pre {
background-color: rgba(10, 10, 10, 0.4);
}
.vscode-high-contrast pre {
background-color: rgb(0, 0, 0);
}
.vscode-high-contrast h1 {
border-color: rgb(0, 0, 0);
}
.vscode-light table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vscode-dark table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.vscode-light h1,
.vscode-light hr,
.vscode-light table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vscode-dark h1,
.vscode-dark hr,
.vscode-dark table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}
</style>
<style>
/* Tomorrow Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
</style>
<style>
/*
* Markdown PDF CSS
*/
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo";
padding: 0 12px;
}
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
pre:not(.hljs) {
padding: 23px;
line-height: 19px;
}
blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
.emoji {
height: 1.4em;
}
code {
font-size: 14px;
line-height: 19px;
}
/* for inline code */
:not(pre):not(.hljs) > code {
color: #C9AE75; /* Change the old color so it seems less like an error */
font-size: inherit;
}
/* Page Break : use <div class="page"/> to insert page break
-------------------------------------------------------- */
.page {
page-break-after: always;
}
</style>
<script src="https://unpkg.com/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<script>
mermaid.initialize({
startOnLoad: true,
theme: document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast')
? 'dark'
: 'default'
});
</script>
<h1 id="suitecoffee--sistema-de-gesti%C3%B3n-para-cafeter%C3%ADas-dockerizado-y-multi%E2%80%91servicio">SuiteCoffee — Sistema de gestión para cafeterías (Dockerizado y multiservicio)</h1>
<p>SuiteCoffee es un sistema modular pensado para la <strong>gestión de cafeterías</strong> (y negocios afines), con servicios Node.js para <strong>aplicación</strong> y <strong>autenticación</strong>, bases de datos <strong>PostgreSQL</strong> separadas para negocio y multitenencia, y un <strong>stack Docker Compose</strong> que facilita levantar entornos de <strong>desarrollo</strong> y <strong>producción</strong>. Incluye herramientas auxiliares como <strong>Nginx Proxy Manager (NPM)</strong> y <strong>CloudBeaver</strong> para administrar bases de datos desde el navegador.</p>
<blockquote>
<p>Repositorio: https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git</p>
</blockquote>
<hr>
<h2 id="tabla-de-contenidos">Tabla de contenidos</h2>
<ul>
<li><a href="#arquitectura">Arquitectura</a></li>
<li><a href="#caracter%C3%ADsticas-principales">Características principales</a></li>
<li><a href="#requisitos">Requisitos</a></li>
<li><a href="#inicio-r%C3%A1pido">Inicio rápido</a></li>
<li><a href="#variables-de-entorno">Variables de entorno</a></li>
<li><a href="#endpoints">Endpoints</a></li>
<li><a href="#estructura-del-proyecto">Estructura del proyecto</a></li>
<li><a href="#herramientas-auxiliares-npm-y-cloudbeaver">Herramientas auxiliares (NPM y CloudBeaver)</a></li>
<li><a href="#backups-y-restauraci%C3%B3n-de-vol%C3%BAmenes">Backups y restauración de volúmenes</a></li>
<li><a href="#comandos-%C3%BAtiles">Comandos útiles</a></li>
<li><a href="#licencia">Licencia</a></li>
<li><a href="#sugerencias-de-mejora">Sugerencias de mejora</a></li>
</ul>
<hr>
<h2 id="arquitectura">Arquitectura</h2>
<p><strong>Servicios principales</strong></p>
<ul>
<li><strong>app</strong> (Node.js / Express): API de negocio y páginas simples para cargar y listar <em>roles, usuarios, categorías y productos</em>.</li>
<li><strong>auth</strong> (Node.js / Express + bcrypt): endpoints de <strong>registro</strong> e <strong>inicio de sesión</strong>.</li>
<li><strong>db</strong> (PostgreSQL 16): base de datos de la aplicación.</li>
<li><strong>tenants</strong> (PostgreSQL 16): base de datos separada para <strong>multi-tenencia</strong> (aislar clientes/tiendas).</li>
</ul>
<p><strong>Herramientas</strong></p>
<ul>
<li><strong>Nginx Proxy Manager (NPM)</strong>: reverse proxy y certificados (Lets Encrypt) para exponer servicios.</li>
<li><strong>CloudBeaver (DBeaver)</strong>: administración de PostgreSQL vía web.</li>
</ul>
<p><strong>Redes &amp; Volúmenes</strong></p>
<ul>
<li>Redes independientes por entorno (<code>suitecoffee_dev_net</code> / <code>suitecoffee_prod_net</code>).</li>
<li>Volúmenes gestionados por Compose para persistencia: <code>suitecoffee-db</code>, <code>tenants-db</code>, etc.</li>
</ul>
<h3 id="diagrama-alto-nivel">Diagrama (alto nivel)</h3>
<pre class="hljs"><code><div>@startuml
skinparam componentStyle rectangle
skinparam rectangle {
BorderColor #555
RoundCorner 10
}
actor Usuario
package &quot;Entorno DEV/PROD&quot; {
[app (Express)] as APP
[auth (Express + bcrypt)] as AUTH
database &quot;db (PostgreSQL)&quot; as DB
database &quot;tenants (PostgreSQL)&quot; as TENANTS
APP -down-&gt; DB : Pool PG
APP -down-&gt; TENANTS : Pool PG
AUTH -down-&gt; DB : Pool PG (usuarios)
Usuario --&gt; APP : UI / API
Usuario --&gt; AUTH : Login/Registro
}
package &quot;Herramientas&quot; {
[Nginx Proxy Manager] as NPM
[CloudBeaver] as DBVR
NPM ..&gt; APP : proxy
NPM ..&gt; AUTH : proxy
DBVR ..&gt; DB : admin
DBVR ..&gt; TENANTS : admin
}
@enduml
</div></code></pre>
<hr>
<h2 id="caracter%C3%ADsticas-principales">Características principales</h2>
<ul>
<li><strong>API REST</strong> para entidades clave (roles, usuarios, categorías y productos).</li>
<li><strong>Autenticación básica</strong> (registro y login) con <strong>hash de contraseñas</strong> (bcrypt).</li>
<li><strong>Multitenencia</strong> con base <code>tenants</code> separada para aislar clientes/tiendas.</li>
<li><strong>Docker Compose v2</strong> con entornos de <strong>desarrollo</strong> y <strong>producción</strong>.</li>
<li><strong>Herramientas integradas</strong> (NPM + CloudBeaver) en un <code>compose.tools.yaml</code> aparte.</li>
<li><strong>Scripts</strong> de <strong>backup/restauración de volúmenes</strong> y <strong>gestión de entornos</strong>.</li>
</ul>
<hr>
<h2 id="requisitos">Requisitos</h2>
<ul>
<li><strong>Docker</strong> y <strong>Docker Compose v2</strong> (recomendado).</li>
<li><strong>Python 3.9+</strong> (para scripts <code>suitecoffee.py</code>, backups y utilidades).</li>
<li><strong>Node.js 20+</strong> (sólo si vas a ejecutar servicios Node fuera de Docker).</li>
</ul>
<hr>
<h2 id="inicio-r%C3%A1pido">Inicio rápido</h2>
<h3 id="opci%C3%B3n-a--gestor-interactivo-recomendado">Opción A — Gestor interactivo (recomendado)</h3>
<ol>
<li>Clona el repo y entra al directorio:<pre class="hljs"><code><div>git <span class="hljs-built_in">clone</span> https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git
<span class="hljs-built_in">cd</span> SuiteCoffee
</div></code></pre>
</li>
<li>(Opcional) Crea/copía tus archivos <code>.env</code> para <strong>app</strong> y <strong>auth</strong> en <code>./services/&lt;service&gt;/.env.development</code> (ver sección de variables).</li>
<li>Ejecuta el gestor:<pre class="hljs"><code><div>python3 suitecoffee.py
</div></code></pre>
<ul>
<li>Verás un <strong>menú</strong> para levantar <strong>DESARROLLO</strong> o <strong>PRODUCCIÓN</strong>.</li>
<li>Desde ahí también puedes <strong>levantar/apagar</strong> las herramientas <strong>NPM</strong> y <strong>CloudBeaver</strong>.</li>
</ul>
</li>
<li>Accede:
<ul>
<li>App (dev): suele estar disponible via NPM o directamente dentro de la red, según tu configuración.</li>
<li>Páginas simples: <code>/roles</code>, <code>/usuarios</code>, <code>/categorias</code>, <code>/productos</code> (servidas por <code>app</code>).</li>
<li>Salud: <code>/health</code> en <code>app</code> y <code>auth</code>.</li>
</ul>
</li>
</ol>
<blockquote>
<p>Consejo: primero levanta <strong>desarrollo/producción</strong> y luego las <strong>herramientas</strong> para que existan las redes externas <code>suitecoffee_dev_net</code>/<code>suitecoffee_prod_net</code> que usa <code>compose.tools.yaml</code>.</p>
</blockquote>
<h3 id="opci%C3%B3n-b--comandos-docker-compose-avanzado">Opción B — Comandos Docker Compose (avanzado)</h3>
<ul>
<li>
<p><strong>Desarrollo</strong>:</p>
<pre class="hljs"><code><div>docker compose -f compose.yaml -f compose.dev.yaml --env-file ./services/app/.env.development --env-file ./services/auth/.env.development -p suitecoffee_dev up -d
</div></code></pre>
</li>
<li>
<p><strong>Producción</strong>:</p>
<pre class="hljs"><code><div>docker compose -f compose.yaml -f compose.prod.yaml --env-file ./services/app/.env.production --env-file ./services/auth/.env.production -p suitecoffee_prod up -d
</div></code></pre>
</li>
</ul>
<blockquote>
<p>Los puertos se <strong>exponen</strong> para herramientas (NPM UI <code>:81</code>, CloudBeaver <code>:8978</code>); los servicios <code>app</code> y <code>auth</code> se <strong>exponen dentro de la red</strong> y se publican externamente a través de NPM.</p>
</blockquote>
<hr>
<h2 id="variables-de-entorno">Variables de entorno</h2>
<p>Crea un archivo <code>.env.development</code> (y uno <code>.env.production</code>) en <strong>cada servicio</strong> (<code>./services/app</code> y <code>./services/auth</code>). Variables comunes:</p>
<pre class="hljs"><code><div># Servidor
PORT=4000 # puerto HTTP del servicio
NODE_ENV=development # development | production
# Base de datos
DB_HOST=db # nombre del servicio postgres (o host)
DB_LOCAL_PORT=5432 # puerto de PG al que conectarse
DB_USER=postgres
DB_PASS=postgres
DB_NAME=suitecoffee_db # para 'db' (aplicación)
TENANTS_DB_NAME=tenants_db # si el servicio necesita apuntar a 'tenants'
</div></code></pre>
<blockquote>
<p>Ajusta <code>DB_HOST</code> a <code>db</code> o <code>tenants</code> según corresponda. En desarrollo, los alias útiles son <code>dev-db</code> y <code>dev-tenants</code>; en producción: <code>prod-db</code> y <code>prod-tenants</code>.</p>
</blockquote>
<hr>
<h2 id="endpoints">Endpoints</h2>
<h3 id="servicio-app-negocio">Servicio <strong>app</strong> (negocio)</h3>
<ul>
<li><code>GET /health</code></li>
<li><code>GET /api/roles</code> — lista roles</li>
<li><code>POST /api/roles</code> — crea un rol</li>
<li><code>GET /api/usuarios</code> — lista usuarios</li>
<li><code>POST /api/usuarios</code> — crea un usuario</li>
<li><code>GET /api/categorias</code> — lista categorías</li>
<li><code>POST /api/categorias</code> — crea una categoría</li>
<li><code>GET /api/productos</code> — lista productos</li>
<li><code>POST /api/productos</code> — crea un producto</li>
<li>Páginas estáticas simples para probar: <code>/roles</code>, <code>/usuarios</code>, <code>/categorias</code>, <code>/productos</code></li>
</ul>
<h3 id="servicio-auth-autenticaci%C3%B3n">Servicio <strong>auth</strong> (autenticación)</h3>
<ul>
<li><code>GET /health</code></li>
<li><code>POST /register</code> — registro de usuario (password con <strong>bcrypt</strong>)</li>
<li><code>POST /auth/login</code> — inicio de sesión</li>
</ul>
<blockquote>
<p><strong>Nota</strong>: En esta etapa los endpoints son <strong>básicos</strong> y pensados para desarrollo/PoC. Ver la sección <em>Sugerencias de mejora</em> para próximos pasos (JWT, autorización, etc.).</p>
</blockquote>
<hr>
<h2 id="estructura-del-proyecto">Estructura del proyecto</h2>
<pre class="hljs"><code><div>SuiteCoffee/
├─ services/
│ ├─ app/
│ │ ├─ src/
│ │ │ ├─ index.js # API y páginas simples
│ │ │ └─ pages/ # roles.html, usuarios.html, categorias.html, productos.html
│ │ ├─ .env.development # variables (ejemplo)
│ │ └─ .env.production
│ └─ auth/
│ ├─ src/
│ │ └─ index.js # /register y /auth/login
│ ├─ .env.development
│ └─ .env.production
├─ compose.yaml # base (db, tenants)
├─ compose.dev.yaml # entorno desarrollo (app, auth, db, tenants)
├─ compose.prod.yaml # entorno producción (app, auth, db, tenants)
├─ compose.tools.yaml # herramientas (NPM, CloudBeaver) con redes externas
├─ suitecoffee.py # gestor interactivo (Docker Compose)
├─ backup_compose_volumes.py # backups de volúmenes Compose
└─ restore_compose_volumes.py# restauración de volúmenes Compose
</div></code></pre>
<hr>
<h2 id="herramientas-auxiliares-npm-y-cloudbeaver">Herramientas auxiliares (NPM y CloudBeaver)</h2>
<p>Los servicios de <strong>herramientas</strong> están separados para poder usarlos con <strong>ambos entornos</strong> (dev y prod) a la vez. Se levantan con <code>compose.tools.yaml</code> y se conectan a las <strong>redes externas</strong> <code>suitecoffee_dev_net</code> y <code>suitecoffee_prod_net</code>.</p>
<ul>
<li><strong>Nginx Proxy Manager (NPM)</strong><br>
Puertos: <code>80</code> (HTTP), <code>81</code> (UI). Volúmenes: <code>npm_data</code>, <code>npm_letsencrypt</code>.</li>
<li><strong>CloudBeaver</strong><br>
Puerto: <code>8978</code>. Volúmenes: <code>dbeaver_logs</code>, <code>dbeaver_workspace</code>.</li>
</ul>
<blockquote>
<p>Si es la primera vez, arranca un entorno (dev/prod) para que Compose cree las redes; luego levanta las herramientas:</p>
<pre class="hljs"><code><div>docker compose -f compose.tools.yaml --profile npm -p suitecoffee up -d
docker compose -f compose.tools.yaml --profile dbeaver -p suitecoffee up -d
</div></code></pre>
</blockquote>
<hr>
<h2 id="backups-y-restauraci%C3%B3n-de-vol%C3%BAmenes">Backups y restauración de volúmenes</h2>
<p>Este repo incluye dos utilidades:</p>
<ul>
<li><code>backup_compose_volumes.py</code> — detecta volúmenes de un proyecto de Compose (por <strong>labels</strong> y nombres) y los exporta a <code>tar.gz</code> usando un contenedor <code>alpine</code> temporal.</li>
<li><code>restore_compose_volumes.py</code> — permite restaurar esos <code>tar.gz</code> en volúmenes (útil para migraciones y pruebas).</li>
</ul>
<p><strong>Ejemplos básicos</strong></p>
<pre class="hljs"><code><div><span class="hljs-comment"># Listar ayuda</span>
python3 backup_compose_volumes.py --<span class="hljs-built_in">help</span>
<span class="hljs-comment"># Respaldar volúmenes asociados a "suitecoffee_dev" en ./backups</span>
python3 backup_compose_volumes.py --project suitecoffee_dev --output ./backups
<span class="hljs-comment"># Restaurar un archivo a un volumen</span>
python3 restore_compose_volumes.py --archive ./backups/suitecoffee_dev_suitecoffee-db-YYYYmmddHHMMSS.tar.gz --volume suitecoffee_dev_suitecoffee-db
</div></code></pre>
<blockquote>
<p>Consejo: si migraste manualmente y ves advertencias tipo “volume ... already exists but was not created by Docker Compose”, considera marcar el volumen como <code>external: true</code> en el YAML o recrearlo para que Compose lo etiquete correctamente.</p>
</blockquote>
<hr>
<h2 id="comandos-%C3%BAtiles">Comandos útiles</h2>
<pre class="hljs"><code><div><span class="hljs-comment"># Ver estado (menú interactivo)</span>
python3 suitecoffee.py
<span class="hljs-comment"># Levantar DEV/PROD por menú (con o sin --force-recreate)</span>
python3 suitecoffee.py
<span class="hljs-comment"># Levantar herramientas (también desde menú)</span>
docker compose -f compose.tools.yaml --profile npm -p suitecoffee up -d
docker compose -f compose.tools.yaml --profile dbeaver -p suitecoffee up -d
<span class="hljs-comment"># Inspeccionar servicios/volúmenes que Compose detecta desde los YAML</span>
docker compose -f compose.yaml -f compose.dev.yaml config --services
docker compose -f compose.yaml -f compose.dev.yaml config --format json | jq .volumes
</div></code></pre>
<hr>
<h2 id="licencia">Licencia</h2>
<ul>
<li><strong>ISC</strong> (ver <code>package.json</code>).</li>
</ul>
<hr>
<h2 id="sugerencias-de-mejora">Sugerencias de mejora</h2>
<ul>
<li><strong>Autenticación y seguridad</strong>
<ul>
<li>Emitir <strong>JWT</strong> en el login y proteger rutas (roles/autorización por perfil).</li>
<li>Configurar <strong>CORS</strong> por orígenes (en dev está abierto; en prod restringir).</li>
<li>Añadir <strong>ratelimit</strong> y <strong>helmet</strong> en Express.</li>
</ul>
</li>
<li><strong>Esquema de datos y migraciones</strong>
<ul>
<li>Añadir migraciones automatizadas (p.ej. <strong>Prisma</strong>, <strong>Knex</strong>, <strong>Sequelize</strong> o SQL versionado) y seeds iniciales.</li>
<li>Clarificar el <strong>modelo multitenant</strong>: por <strong>BD por tenant</strong> o <strong>schema por tenant</strong>; documentar estrategia.</li>
</ul>
</li>
<li><strong>Calidad &amp; DX</strong>
<ul>
<li>Tests (unitarios e integración) y <strong>CI</strong> básico.</li>
<li>Validación de entrada (<strong>zod / joi</strong>), manejo de errores consistente y logs estructurados.</li>
</ul>
</li>
<li><strong>Docker/DevOps</strong>
<ul>
<li>Documentar variables <code>.env</code> completas por servicio.</li>
<li>Publicar imágenes de producción y usar <code>IMAGE:TAG</code> en <code>compose.prod.yaml</code> (evitar build en servidor).</li>
<li>Añadir <strong>healthchecks</strong> a <code>app</code>/<code>auth</code> (ya hay ejemplos comentados).</li>
</ul>
</li>
<li><strong>Frontend</strong>
<ul>
<li>Reemplazar páginas HTML de prueba por un <strong>frontend</strong> (React/Vite) o una UI admin mínima.</li>
</ul>
</li>
<li><strong>Pequeños fixes</strong>
<ul>
<li>En los HTML de ejemplo corregir las referencias a campos <code>id_rol</code>, <code>id_categoria</code>, etc.</li>
<li>Centralizar constantes (nombres de tablas/campos) y normalizar respuestas API.</li>
</ul>
</li>
</ul>
<hr>
</body>
</html>