412 lines
18 KiB
Markdown
412 lines
18 KiB
Markdown
|
|
# Inventschario — Documento de Arquitectura
|
||
|
|
|
||
|
|
**Versión:** 1.0.0-iteracion1
|
||
|
|
**Fecha:** 2026-08-03
|
||
|
|
**Estado:** Propuesta para revisión
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Visión General
|
||
|
|
|
||
|
|
Inventschario采用 arquitectura de tres capas con separación clara entre
|
||
|
|
presentación, lógica de negocio y persistencia. La capa de presentación se
|
||
|
|
ejecuta en el navegador del usuario; la capa de negocio y persistencia corren
|
||
|
|
como un servidor local HTTP que solo escucha en `127.0.0.1`.
|
||
|
|
|
||
|
|
La pasarela entre frontend y backend es una API REST documentada, de forma que
|
||
|
|
el frontend puede sustituirse completamente sin modificar ninguna línea del
|
||
|
|
backend, y viceversa.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Diagrama de Componentes
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ NAVEGADOR (Edge) │
|
||
|
|
│ │
|
||
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ FRONTEND │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||
|
|
│ │ │Dashboard │ │Inventario│ │ Formular │ │ Config │ │ │
|
||
|
|
│ │ │ Page │ │ Page │ │ ios │ │ Page │ │ │
|
||
|
|
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
||
|
|
│ │ │ │ │ │ │ │
|
||
|
|
│ │ ┌────┴──────────────┴──────────────┴──────────────┴────┐ │
|
||
|
|
│ │ │ Router (SPA) │ │
|
||
|
|
│ │ └────────────────────┬─────────────────────────────────┘ │ │
|
||
|
|
│ │ │ │ │
|
||
|
|
│ │ ┌────────────────────┴─────────────────────────────────┐ │
|
||
|
|
│ │ │ API Client (fetch wrapper) │ │
|
||
|
|
│ │ └────────────────────┬─────────────────────────────────┘ │ │
|
||
|
|
│ │ │ │ │
|
||
|
|
│ │ ┌────────────────────┴─────────────────────────────────┐ │
|
||
|
|
│ │ │ Component Library │ │
|
||
|
|
│ │ │ Table | Form | Modal | Toast | Sidebar | Charts │ │
|
||
|
|
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||
|
|
│ └───────────────────────────┬───────────────────────────────┘ │
|
||
|
|
│ │ HTTP localhost:PORT │
|
||
|
|
├──────────────────────────────┼──────────────────────────────────┤
|
||
|
|
│ │ │
|
||
|
|
│ ┌───────────────────────────┴───────────────────────────────┐ │
|
||
|
|
│ │ API GATEWAY │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||
|
|
│ │ │ /devices │ │ /import │ │ /backups │ │ /config │ │ │
|
||
|
|
│ │ │ /history │ │ /export │ │ │ │ /labels │ │ │
|
||
|
|
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
||
|
|
│ │ │ │ │ │ │ │
|
||
|
|
│ │ ┌────┴──────────────┴──────────────┴──────────────┴────┐ │ │
|
||
|
|
│ │ │ Middleware Layer │ │ │
|
||
|
|
│ │ │ CORS (localhost) | JSON parsing | Error handling │ │ │
|
||
|
|
│ │ └────────────────────┬─────────────────────────────────┘ │ │
|
||
|
|
│ └───────────────────────┼───────────────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌───────────────────────┴───────────────────────────────────┐ │
|
||
|
|
│ │ SERVICE LAYER │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
|
|
│ │ │DeviceService │ │BackupService │ │ImportService │ │ │
|
||
|
|
│ │ │ │ │ │ │ │ │ │
|
||
|
|
│ │ │- CRUD │ │- crear │ │- parse CSV │ │ │
|
||
|
|
│ │ │- buscar │ │- restaurar │ │- mapear │ │ │
|
||
|
|
│ │ │- filtrar │ │- eliminar │ │- validar │ │ │
|
||
|
|
│ │ │- baja │ │- verificar │ │- duplicados │ │ │
|
||
|
|
│ │ │- enajenar │ │ antigüedad │ │ │ │ │
|
||
|
|
│ │ │- historial │ │ │ │ │ │ │
|
||
|
|
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
|
||
|
|
│ │ │ │ │ │ │
|
||
|
|
│ │ ┌──────┴──────────────────┴──────────────────┴───────┐ │ │
|
||
|
|
│ │ │ ExportService | ConfigService │ │ │
|
||
|
|
│ │ └──────────────────────┬─────────────────────────────┘ │ │
|
||
|
|
│ └─────────────────────────┼─────────────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌─────────────────────────┴─────────────────────────────────┐ │
|
||
|
|
│ │ DATA LAYER │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
|
|
│ │ │ DeviceRepo │ │ ConfigRepo │ │ BackupRepo │ │ │
|
||
|
|
│ │ │ │ │ │ │ │ │ │
|
||
|
|
│ │ │ SQLite CRUD │ │ KV Store │ │ Log CRUD │ │ │
|
||
|
|
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
|
||
|
|
│ │ │ │ │ │ │
|
||
|
|
│ │ ┌──────┴──────────────────┴──────────────────┴───────┐ │ │
|
||
|
|
│ │ │ SQLite (WAL mode) │ │ │
|
||
|
|
│ │ │ inventschario.db │ │ │
|
||
|
|
│ │ └────────────────────────────────────────────────────┘ │ │
|
||
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Alternativas de Arquitectura
|
||
|
|
|
||
|
|
### Alternativa A: Python + Flask (Recomendada)
|
||
|
|
|
||
|
|
```
|
||
|
|
[Browser] ──HTTP──► [Flask server] ──► [SQLite]
|
||
|
|
│ │
|
||
|
|
│ HTML/CSS/JS │ Python services
|
||
|
|
│ (Alpine.js) │ (DeviceService, etc.)
|
||
|
|
│ │
|
||
|
|
◄──── JSON ──────────►│
|
||
|
|
```
|
||
|
|
|
||
|
|
**Por qué se recomienda:**
|
||
|
|
- Python stdlib incluye sqlite3, http.server, json, gzip, csv, shutil
|
||
|
|
- Flask es ligero (~500KB) y no necesita build step
|
||
|
|
- PyInstaller empaqueta todo en un .exe autocontenido
|
||
|
|
- El ecosistema Python tiene librerías para todo lo necesario
|
||
|
|
- Desarrollo rápido, fácil de mantener
|
||
|
|
|
||
|
|
**Componentes:**
|
||
|
|
- `flask` — Servidor web minimalista
|
||
|
|
- `schedule` — Programador de tareas (backups diarios)
|
||
|
|
- `pyinstaller` — Empaquetado en .exe
|
||
|
|
|
||
|
|
### Alternativa B: .NET 8 Minimal API
|
||
|
|
|
||
|
|
```
|
||
|
|
[Browser] ──HTTP──► [Kestrel server] ──► [SQLite via EF Core]
|
||
|
|
│ │
|
||
|
|
│ HTML/CSS/JS │ C# services
|
||
|
|
│ (embebido) │ (DeviceService, etc.)
|
||
|
|
│ │
|
||
|
|
◄──── JSON ──────────►│
|
||
|
|
```
|
||
|
|
|
||
|
|
**Por qué es alternativa válida:**
|
||
|
|
- `dotnet publish -r win-x64 --self-contained` produce .exe sin runtime
|
||
|
|
- Rendimiento superior a Python en operaciones intensivas
|
||
|
|
- Integración nativa con Windows
|
||
|
|
- Entity Framework Core para ORM robusto
|
||
|
|
|
||
|
|
**Componentes:**
|
||
|
|
- ASP.NET Core Minimal API
|
||
|
|
- Microsoft.Data.Sqlite
|
||
|
|
- Hangfire (tareas programadas)
|
||
|
|
|
||
|
|
### Alternativa C: Go + net/http
|
||
|
|
|
||
|
|
```
|
||
|
|
[Browser] ──HTTP──► [Go HTTP server] ──► [SQLite via CGO]
|
||
|
|
│ │
|
||
|
|
│ HTML/CSS/JS │ Go handlers
|
||
|
|
│ (embed.FS) │ (DeviceService, etc.)
|
||
|
|
│ │
|
||
|
|
◄──── JSON ──────────►│
|
||
|
|
```
|
||
|
|
|
||
|
|
**Por qué es alternativa válida:**
|
||
|
|
- Binario más pequeño (10-15 MB)
|
||
|
|
- Rendimiento excepcional
|
||
|
|
- Sin dependencias en runtime
|
||
|
|
- `embed.FS` embebe archivos estáticos directamente en el binario
|
||
|
|
|
||
|
|
**Componentes:**
|
||
|
|
- `net/http` (stdlib)
|
||
|
|
- `mattn/go-sqlite3` (CGO)
|
||
|
|
- `embed.FS` (archivos estáticos)
|
||
|
|
|
||
|
|
### Decisión: Python + Flask
|
||
|
|
|
||
|
|
Para la primera iteración, Python + Flask es la opción que mejor equilibra:
|
||
|
|
- Velocidad de desarrollo
|
||
|
|
- Facilidad de distribución (PyInstaller)
|
||
|
|
- Modularidad del frontend (API REST pura)
|
||
|
|
- Ecosistema de herramientas
|
||
|
|
|
||
|
|
Si en iteraciones futuras el tamaño del .exe o la detección por antivirus
|
||
|
|
se convierten en problemas, se documentará la migración a .NET 8.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Patrones de Diseño
|
||
|
|
|
||
|
|
### 4.1 Repository Pattern (Capa de Datos)
|
||
|
|
|
||
|
|
Cada tabla tiene su repository que encapsula las consultas SQL:
|
||
|
|
|
||
|
|
```python
|
||
|
|
class DeviceRepository:
|
||
|
|
def __init__(self, db_path):
|
||
|
|
self.db_path = db_path
|
||
|
|
|
||
|
|
def get_all(self, filters=None): ...
|
||
|
|
def get_by_id(self, device_id): ...
|
||
|
|
def create(self, device_data): ...
|
||
|
|
def update(self, device_id, device_data): ...
|
||
|
|
def delete(self, device_id): ...
|
||
|
|
def search(self, query): ...
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 Service Layer (Lógica de Negocio)
|
||
|
|
|
||
|
|
Los services orquestan la lógica sin conocer la API HTTP:
|
||
|
|
|
||
|
|
```python
|
||
|
|
class DeviceService:
|
||
|
|
def __init__(self, device_repo, history_repo):
|
||
|
|
self.device_repo = device_repo
|
||
|
|
self.history_repo = history_repo
|
||
|
|
|
||
|
|
def create_device(self, data, user):
|
||
|
|
device = self.device_repo.create(data)
|
||
|
|
self.history_repo.log(device.id, 'created', user=user)
|
||
|
|
return device
|
||
|
|
|
||
|
|
def decommission(self, device_id, reason, user):
|
||
|
|
device = self.device_repo.get_by_id(device_id)
|
||
|
|
self.device_repo.update_status(device_id, 'decommissioned')
|
||
|
|
self.history_repo.log(device_id, 'decommissioned',
|
||
|
|
notes=reason, user=user)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 API Gateway (Capa HTTP)
|
||
|
|
|
||
|
|
Las rutas API son delgadas — solo parsean la request, llaman al service,
|
||
|
|
y formatean la response:
|
||
|
|
|
||
|
|
```python
|
||
|
|
@bp.route('/api/v1/devices', methods=['POST'])
|
||
|
|
def create_device():
|
||
|
|
data = request.get_json()
|
||
|
|
device = device_service.create_device(data, current_user)
|
||
|
|
return jsonify(device.to_dict()), 201
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.4 Frontend Component Architecture
|
||
|
|
|
||
|
|
El frontend usa un patrón de componentes simples sin framework pesado:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Cada componente es un objeto con render() y bind()
|
||
|
|
const DeviceTable = {
|
||
|
|
render(devices) { return `<table>...</table>`; },
|
||
|
|
bind(container) { /* event listeners */ }
|
||
|
|
};
|
||
|
|
|
||
|
|
// El router cambia de página
|
||
|
|
const Router = {
|
||
|
|
routes: { '/': Dashboard, '/inventory': Inventory },
|
||
|
|
navigate(path) { /* render the page */ }
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.5 Import/Export Plugin Architecture
|
||
|
|
|
||
|
|
El sistema de importación exportación es un plugin architecture:
|
||
|
|
|
||
|
|
```python
|
||
|
|
class ImporterBase:
|
||
|
|
"""Clase abstracta para todos los importadores"""
|
||
|
|
def parse(self, file_path) -> list[dict]: ...
|
||
|
|
def validate(self, records) -> ValidationResult: ...
|
||
|
|
def import_records(self, records, strategy) -> ImportResult: ...
|
||
|
|
|
||
|
|
class CSVImporter(ImporterBase):
|
||
|
|
"""Implementación CSV"""
|
||
|
|
def parse(self, file_path):
|
||
|
|
# Lee CSV, mapea columnas, retorna lista de dicts
|
||
|
|
...
|
||
|
|
|
||
|
|
class FieldMapper:
|
||
|
|
"""Motor de mapeo de campos"""
|
||
|
|
def __init__(self, mapping_config):
|
||
|
|
self.mapping = mapping_config
|
||
|
|
|
||
|
|
def map_record(self, raw_record) -> dict:
|
||
|
|
# Aplica el mapeo configurado
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
Para añadir un nuevo formato (ej: Excel), se crea una nueva clase que
|
||
|
|
herede de `ImporterBase` sin tocar el resto del sistema.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Seguridad
|
||
|
|
|
||
|
|
### 5.1 Red
|
||
|
|
|
||
|
|
- El servidor Flask escucha SOLO en `127.0.0.1:PORT`
|
||
|
|
- No hay exposición a la red local o internet
|
||
|
|
- El puerto se asigna dinámicamente o se configura en el wizard
|
||
|
|
|
||
|
|
### 5.2 Datos
|
||
|
|
|
||
|
|
- SQLite WAL mode para integridad
|
||
|
|
- Foreign keys habilitadas
|
||
|
|
- Validación de datos en backend (no confiar en el frontend)
|
||
|
|
- Backups almacenados en `%APPDATA%` (acceso restringido al usuario)
|
||
|
|
|
||
|
|
### 5.3 Archivos
|
||
|
|
|
||
|
|
- No se ejecutan comandos del sistema
|
||
|
|
- No se escriben archivos fuera de `%APPDATA%/inventschario/`
|
||
|
|
- La BD se almacena en `%APPDATA%/inventschario/inventschario.db`
|
||
|
|
- Los logs se almacenan en `%APPDATA%/inventschario/logs/`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Rendimiento
|
||
|
|
|
||
|
|
### 6.1 Consideraciones
|
||
|
|
|
||
|
|
- Todo es local: latencia de red ~0ms
|
||
|
|
- SQLite es suficiente para inventarios de hasta 100,000 dispositivos
|
||
|
|
- WAL mode permite lecturas concurrentes
|
||
|
|
- Índices en campos de búsqueda frecuentes
|
||
|
|
|
||
|
|
### 6.2 Métricas Objetivo
|
||
|
|
|
||
|
|
| Operación | Tiempo objetivo |
|
||
|
|
|--------------------------------|-----------------|
|
||
|
|
| Carga de página inicial | < 2s |
|
||
|
|
| Búsqueda en inventario | < 500ms |
|
||
|
|
| Crear dispositivo | < 100ms |
|
||
|
|
| Exportar 1,000 dispositivos | < 3s |
|
||
|
|
| Importar 1,000 dispositivos | < 5s |
|
||
|
|
| Backup de BD de 10MB | < 2s |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Estrategia de Testing
|
||
|
|
|
||
|
|
### 7.1 Niveles
|
||
|
|
|
||
|
|
1. **Unit Tests** — Services, repositories, utilidades
|
||
|
|
2. **Integration Tests** — API endpoints con BD en memoria
|
||
|
|
3. **E2E Tests** — Flujo completo (futuro, iteración 2)
|
||
|
|
|
||
|
|
### 7.2 Herramientas
|
||
|
|
|
||
|
|
- `pytest` — Framework de testing
|
||
|
|
- `pytest-flask` — Fixtures para Flask
|
||
|
|
- `sqlite3` en memoria — BD de test aislada
|
||
|
|
|
||
|
|
### 7.3 Cobertura
|
||
|
|
|
||
|
|
Objetivo iteración 1: > 80% cobertura en services y repositories.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Despliegue
|
||
|
|
|
||
|
|
### 8.1 Distribución
|
||
|
|
|
||
|
|
```
|
||
|
|
inventschario/
|
||
|
|
├── inventschario.exe # Ejecutable empaquetado (PyInstaller)
|
||
|
|
├── inventschario.ico # Icono de la aplicación
|
||
|
|
└── README.md # Instrucciones de uso
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.2 Instalación (Usuario)
|
||
|
|
|
||
|
|
1. El usuario ejecuta `inventschario.exe`
|
||
|
|
2. PyInstaller extrae a un directorio temporal
|
||
|
|
3. La aplicación se instala en `%LOCALAPPDATA%/inventschario/`
|
||
|
|
4. Se crea un acceso directo en el Menú Inicio del usuario
|
||
|
|
5. Se lanza el navegador con la aplicación
|
||
|
|
|
||
|
|
### 8.3 Start Menu
|
||
|
|
|
||
|
|
El acceso directo se crea en:
|
||
|
|
```
|
||
|
|
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Inventschario/Inventschario.lnk
|
||
|
|
```
|
||
|
|
|
||
|
|
Esto requiere privilegios de instalador (que el usuario tiene).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Plan de Migración (Si es necesario)
|
||
|
|
|
||
|
|
Si Python + Flask no cumple los requisitos en producción:
|
||
|
|
|
||
|
|
### De Python a .NET 8
|
||
|
|
|
||
|
|
1. Mantener la misma API REST (los endpoints no cambian)
|
||
|
|
2. Reescribir los services en C#
|
||
|
|
3. Usar Entity Framework Core con SQLite
|
||
|
|
4. Publicar como self-contained: `dotnet publish -r win-x64 --self-contained`
|
||
|
|
5. El frontend NO cambia (solo se sirve desde wwwroot)
|
||
|
|
|
||
|
|
### De Python a Go
|
||
|
|
|
||
|
|
1. Mantener la misma API REST
|
||
|
|
2. Reescribir los services en Go
|
||
|
|
3. Usar `mattn/go-sqlite3`
|
||
|
|
4. Compilar como binario estático
|
||
|
|
5. Embeber frontend con `embed.FS`
|
||
|
|
|
||
|
|
En ambos casos, la API REST como contrato permite el cambio de backend
|
||
|
|
sin modificar el frontend.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*Documento generado como parte de la primera iteración del proyecto Inventschario.*
|