feat: roadmap y diseño inicial — iteración 1 antes de feedback del usuario

Inventario de dispositivos electrónicos no prestables para administración
pública. Esta primera iteración contiene el diseño y la planificación
antes de contactar con el usuario para su validación.

Contenido:
- especificaciones.md: requisitos detallados del programa
- docs/arquitectura.md: propuesta de arquitectura con alternativas
- docs/roadmap-iteracion-1.md: plan de desarrollo iteración 1
- docs/arquitectura-diagrama.html: diagrama visual de arquitectura
- sketches/: mockups de las 4 pantallas principales
  - 001-dashboard: vista resumen
  - 002-inventario: tabla de dispositivos
  - 003-formulario: alta/edición de dispositivos
  - 004-configuracion: ajustes generales

Stack propuesto: Python + Flask + SQLite + Alpine.js
Distribución: PyInstaller → un único .exe autocontenido
Entorno: Windows 11 Pro unida a dominio, sin privilegios de admin
This commit is contained in:
pikaos 2026-08-03 01:34:56 +01:00
commit ee849eb9a8
14 changed files with 3761 additions and 0 deletions

View file

@ -0,0 +1,20 @@
# Variant: Configuración — Configure Surface
## Design stance
Tabbed settings with inline editing: each section is self-contained, changes
are saved explicitly, and the field label editor shows the internal key alongside
the user-facing label.
## Key choices
- **Layout:** Tabbed navigation, settings sections as cards, inline list editors
- **Typography:** Monospace for field keys, Inter for labels and values
- **Color:** Minimal — toggle switches for boolean settings, danger color for delete
- **Interaction:** Toggle switches, inline add/remove for locations/departments
## Trade-offs
- Strong at: Organized settings, clear field mapping, backup management
- Weak at: Many settings could overwhelm on first visit
## Best for
Initial setup and ongoing configuration — the admin sets up the system once
and revisits occasionally for adjustments.

View file

@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Configuración</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #1a1a1a; background: #f7f7f5; line-height: 1.5;
display: flex; min-height: 100vh;
}
.sidebar {
width: 240px; background: #fff; border-right: 1px solid #e5e5e3;
padding: 20px 0; flex-shrink: 0; display: flex; flex-direction: column;
}
.sidebar-logo { padding: 0 20px 20px; border-bottom: 1px solid #e5e5e3; margin-bottom: 8px; }
.sidebar-logo h1 { font-size: 16px; font-weight: 700; }
.sidebar-logo span { font-size: 11px; color: #999; display: block; margin-top: 2px; }
.nav-section { padding: 8px 12px; }
.nav-label { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 8px 4px; }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 6px; font-size: 14px; color: #666; cursor: pointer;
text-decoration: none; transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: #f0f0ee; color: #1a1a1a; }
.nav-item.active { background: #f0f0ee; color: #1a1a1a; font-weight: 500; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-spacer { flex: 1; }
.nav-footer { padding: 12px 20px; border-top: 1px solid #e5e5e3; font-size: 12px; color: #999; }
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
background: #fff; border-bottom: 1px solid #e5e5e3;
padding: 16px 32px; display: flex; align-items: center; justify-content: space-between;
}
.topbar h2 { font-size: 18px; font-weight: 600; }
.btn {
padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid #e5e5e3; background: #fff; color: #1a1a1a;
transition: background 0.15s; font-family: inherit;
}
.btn:hover { background: #f0f0ee; }
.btn-primary { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
.btn-primary:hover { background: #333; }
.btn-danger { color: #dc2626; border-color: #fca5a5; }
.btn-danger:hover { background: #fef2f2; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.content { padding: 32px; overflow-y: auto; flex: 1; max-width: 800px; }
/* Settings sections */
.settings-section {
background: #fff; border: 1px solid #e5e5e3; border-radius: 8px;
margin-bottom: 24px; overflow: hidden;
}
.settings-header {
padding: 16px 20px; border-bottom: 1px solid #e5e5e3;
display: flex; align-items: center; justify-content: space-between;
}
.settings-header h3 { font-size: 14px; font-weight: 600; }
.settings-header p { font-size: 12px; color: #999; }
.settings-body { padding: 20px; }
.setting-row {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 0; border-bottom: 1px solid #f0f0ee;
}
.setting-row:last-child { border-bottom: none; }
.setting-info { flex: 1; }
.setting-label { font-size: 13px; font-weight: 500; }
.setting-desc { font-size: 12px; color: #999; margin-top: 2px; }
.setting-control { margin-left: 20px; }
.form-input, .form-select {
padding: 6px 10px; border: 1px solid #e5e5e3; border-radius: 4px;
font-size: 13px; font-family: inherit; background: #fff;
}
.form-input:focus, .form-select:focus {
outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a;
}
/* Toggle switch */
.toggle {
width: 40px; height: 22px; background: #e5e5e3; border-radius: 11px;
position: relative; cursor: pointer; transition: background 0.2s;
border: none; padding: 0;
}
.toggle.active { background: #166534; }
.toggle::after {
content: ''; width: 18px; height: 18px; background: #fff;
border-radius: 50%; position: absolute; top: 2px; left: 2px;
transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.toggle.active::after { transform: translateX(18px); }
/* Field labels editor */
.label-editor { margin-top: 8px; }
.label-row {
display: flex; align-items: center; gap: 12px; padding: 8px 0;
border-bottom: 1px solid #f0f0ee;
}
.label-row:last-child { border-bottom: none; }
.label-key {
font-size: 12px; color: #999; font-family: monospace;
min-width: 160px;
}
.label-input {
flex: 1; padding: 6px 10px; border: 1px solid #e5e5e3; border-radius: 4px;
font-size: 13px; font-family: inherit;
}
.label-input:focus { outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a; }
/* Backup list */
.backup-list { margin-top: 12px; }
.backup-item {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 12px; border: 1px solid #f0f0ee; border-radius: 6px;
margin-bottom: 8px; font-size: 13px;
}
.backup-info { display: flex; align-items: center; gap: 12px; }
.backup-icon { font-size: 16px; }
.backup-name { font-weight: 500; }
.backup-meta { font-size: 11px; color: #999; }
.backup-actions { display: flex; gap: 4px; }
/* Tabs */
.tabs {
display: flex; gap: 0; border-bottom: 1px solid #e5e5e3; margin-bottom: 20px;
}
.tab {
padding: 10px 16px; font-size: 13px; font-weight: 500; color: #666;
cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s;
background: none; border-top: none; border-left: none; border-right: none;
font-family: inherit;
}
.tab:hover { color: #1a1a1a; }
.tab.active { color: #1a1a1a; border-bottom-color: #1a1a1a; }
*:focus-visible { outline: 2px solid #1a1a1a; outline-offset: 2px; }
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" role="navigation" aria-label="Menú principal">
<div class="sidebar-logo"><h1>Inventschario</h1><span>Inventario Electrónico</span></div>
<div class="nav-section">
<div class="nav-label">Principal</div>
<a class="nav-item" href="#dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item" href="#inventory">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
Inventario
</a>
<a class="nav-item" href="#add">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Agregar Dispositivo
</a>
</div>
<div class="nav-section">
<div class="nav-label">Herramientas</div>
<a class="nav-item" href="#import">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar / Exportar
</a>
<a class="nav-item" href="#backups">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Copias de Seguridad
</a>
<a class="nav-item active" href="#settings" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Configuración
</a>
</div>
<div class="nav-spacer"></div>
<div class="nav-footer">v1.0.0 — 2026-08-03</div>
</nav>
<!-- Main Content -->
<main class="main" role="main">
<header class="topbar">
<h2>Configuración</h2>
<div class="topbar-actions">
<button class="btn btn-primary">Guardar Cambios</button>
</div>
</header>
<div class="content">
<!-- Tabs -->
<div class="tabs" role="tablist">
<button class="tab active" role="tab" aria-selected="true">General</button>
<button class="tab" role="tab">Campos</button>
<button class="tab" role="tab">Copias de Seguridad</button>
<button class="tab" role="tab">Importar / Exportar</button>
</div>
<!-- General Settings -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Información General</h3>
<p>Datos de la institución y configuración básica</p>
</div>
</div>
<div class="settings-body">
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Nombre de la Institución</div>
<div class="setting-desc">Aparece en el encabezado de exportaciones e informes</div>
</div>
<div class="setting-control">
<input class="form-input" type="text" value="Ayuntamiento de Ejemplo" style="width: 280px;">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Prefijo de Número de Catálogo</div>
<div class="setting-desc">Prefijo para la generación automática de números de inventario</div>
</div>
<div class="setting-control">
<input class="form-input" type="text" value="INV-" style="width: 120px;">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Separador CSV por Defecto</div>
<div class="setting-desc">Separador utilizado al exportar e importar archivos CSV</div>
</div>
<div class="setting-control">
<select class="form-select">
<option value=";">Punto y coma (;)</option>
<option value=",">Coma (,)</option>
<option value="\t">Tabulador</option>
</select>
</div>
</div>
</div>
</div>
<!-- Field Labels -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Etiquetas de Campos</h3>
<p>Personaliza los nombres que aparecen en la interfaz. El nombre interno en la base de datos no cambia.</p>
</div>
</div>
<div class="settings-body">
<div class="label-editor">
<div class="label-row">
<span class="label-key">serial_number</span>
<input class="label-input" type="text" value="Número de Serie">
</div>
<div class="label-row">
<span class="label-key">service_tag</span>
<input class="label-input" type="text" value="Service Tag">
</div>
<div class="label-row">
<span class="label-key">catalog_number</span>
<input class="label-input" type="text" value="Número de Catálogo Interno">
</div>
<div class="label-row">
<span class="label-key">additional_registry</span>
<input class="label-input" type="text" value="Registro Adicional">
</div>
<div class="label-row">
<span class="label-key">device_name</span>
<input class="label-input" type="text" value="Nombre / Descripción">
</div>
<div class="label-row">
<span class="label-key">device_type</span>
<input class="label-input" type="text" value="Tipo de Dispositivo">
</div>
<div class="label-row">
<span class="label-key">brand</span>
<input class="label-input" type="text" value="Marca">
</div>
<div class="label-row">
<span class="label-key">model</span>
<input class="label-input" type="text" value="Modelo">
</div>
<div class="label-row">
<span class="label-key">purchase_date</span>
<input class="label-input" type="text" value="Fecha de Compra">
</div>
<div class="label-row">
<span class="label-key">warranty_expiry</span>
<input class="label-input" type="text" value="Fin de Garantía">
</div>
<div class="label-row">
<span class="label-key">purchase_price</span>
<input class="label-input" type="text" value="Precio de Compra (€)">
</div>
<div class="label-row">
<span class="label-key">assigned_location</span>
<input class="label-input" type="text" value="Ubicación Asignada">
</div>
<div class="label-row">
<span class="label-key">assigned_user</span>
<input class="label-input" type="text" value="Usuario Asignado">
</div>
<div class="label-row">
<span class="label-key">department</span>
<input class="label-input" type="text" value="Departamento">
</div>
<div class="label-row">
<span class="label-key">status</span>
<input class="label-input" type="text" value="Estado">
</div>
<div class="label-row">
<span class="label-key">condition</span>
<input class="label-input" type="text" value="Estado Físico">
</div>
</div>
</div>
</div>
<!-- Backup Settings -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Copias de Seguridad</h3>
<p>Configuración del backup automático y gestión de copias</p>
</div>
</div>
<div class="settings-body">
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Backup Automático</div>
<div class="setting-desc">Crear una copia de seguridad de la base de datos cada día</div>
</div>
<div class="setting-control">
<button class="toggle active" role="switch" aria-checked="true" aria-label="Activar backup automático"></button>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Hora del Backup</div>
<div class="setting-desc">Hora a la que se ejecuta el backup diario</div>
</div>
<div class="setting-control">
<input class="form-input" type="time" value="02:00">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Días de Retención</div>
<div class="setting-desc">Número de días antes de sugerir la eliminación de backups antiguos</div>
</div>
<div class="setting-control">
<input class="form-input" type="number" value="30" min="7" max="365" style="width: 80px;">
<span style="font-size: 12px; color: #999; margin-left: 4px;">días</span>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Máximo de Backups</div>
<div class="setting-desc">Número máximo de copias de seguridad a conservar</div>
</div>
<div class="setting-control">
<input class="form-input" type="number" value="90" min="10" max="365" style="width: 80px;">
</div>
</div>
<!-- Recent backups -->
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #e5e5e3;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Copias Recientes</span>
<button class="btn btn-sm">Crear Backup Manual</button>
</div>
<div class="backup-list">
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260803_020000.db.gz</div>
<div class="backup-meta">3 de agosto 2026, 02:00 — 2.4 MB — Automático</div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm" aria-label="Restaurar este backup">Restaurar</button>
<button class="btn btn-sm btn-danger" aria-label="Eliminar este backup">Eliminar</button>
</div>
</div>
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260802_020000.db.gz</div>
<div class="backup-meta">2 de agosto 2026, 02:00 — 2.3 MB — Automático</div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm">Restaurar</button>
<button class="btn btn-sm btn-danger">Eliminar</button>
</div>
</div>
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260801_143022.db.gz</div>
<div class="backup-meta">1 de agosto 2026, 14:30 — 2.3 MB — <strong style="color: #92400e;">Manual</strong></div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm">Restaurar</button>
<button class="btn btn-sm btn-danger">Eliminar</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Locations & Departments -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Ubicaciones y Departamentos</h3>
<p>Gestiona las ubicaciones físicas y departamentos de la institución</p>
</div>
</div>
<div class="settings-body">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
<!-- Locations -->
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Ubicaciones</span>
<button class="btn btn-sm">+ Añadir</button>
</div>
<div style="border: 1px solid #e5e5e3; border-radius: 6px; overflow: hidden;">
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 0 — Recepción</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 1 — Open Space</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 1 — Reprografía</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 2 — Desp. Dirección</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; font-size: 13px; display: flex; justify-content: space-between;">
<span>Almacén</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
</div>
</div>
<!-- Departments -->
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Departamentos</span>
<button class="btn btn-sm">+ Añadir</button>
</div>
<div style="border: 1px solid #e5e5e3; border-radius: 6px; overflow: hidden;">
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Informática</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Administración</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Recursos Humanos</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Contabilidad</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; font-size: 13px; display: flex; justify-content: space-between;">
<span>Dirección</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>