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,19 @@
# Variant: Formulario — Configure Surface
## Design stance
Progressive disclosure through logical sections: identification, device info,
purchase, assignment, notes. Required fields marked clearly, inline validation.
## Key choices
- **Layout:** Single-column form card, 2-column grid within sections
- **Typography:** Clear section headers, field labels with required markers
- **Color:** Minimal — white card on light gray background, red for required/errors
- **Interaction:** Real-time validation, "Save and Add Another" for batch entry
## Trade-offs
- Strong at: Clear data entry flow, reduced errors, batch-friendly
- Weak at: Long form on small screens (needs responsive stacking)
## Best for
Staff entering new devices — the form guides them through required fields
and offers shortcuts for batch entry.

View file

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Agregar Dispositivo</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; }
.topbar-actions { display: flex; gap: 10px; }
.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-ghost { border: none; background: transparent; color: #666; }
.btn-ghost:hover { color: #1a1a1a; background: #f0f0ee; }
.content { padding: 32px; overflow-y: auto; flex: 1; max-width: 800px; }
/* Form */
.form-card {
background: #fff; border: 1px solid #e5e5e3; border-radius: 8px;
overflow: hidden;
}
.form-header {
padding: 20px 24px; border-bottom: 1px solid #e5e5e3;
}
.form-header h3 { font-size: 15px; font-weight: 600; }
.form-header p { font-size: 13px; color: #666; margin-top: 4px; }
.form-body { padding: 24px; }
.form-section {
margin-bottom: 28px;
}
.form-section:last-child { margin-bottom: 0; }
.form-section-title {
font-size: 12px; font-weight: 600; color: #999; text-transform: uppercase;
letter-spacing: 0.5px; margin-bottom: 16px; padding-bottom: 8px;
border-bottom: 1px solid #f0f0ee;
}
.form-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
.form-grid .full-width { grid-column: 1 / -1; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-label {
font-size: 13px; font-weight: 500; color: #1a1a1a;
}
.form-label .required { color: #dc2626; margin-left: 2px; }
.form-hint { font-size: 11px; color: #999; }
.form-input, .form-select, .form-textarea {
padding: 8px 12px; border: 1px solid #e5e5e3; border-radius: 6px;
font-size: 13px; font-family: inherit; background: #fff; color: #1a1a1a;
transition: border-color 0.15s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a;
}
.form-input.error { border-color: #dc2626; }
.form-error { font-size: 11px; color: #dc2626; margin-top: 2px; }
.form-textarea { resize: vertical; min-height: 80px; }
.form-select { cursor: pointer; }
/* Required fields note */
.required-note {
font-size: 12px; color: #999; margin-bottom: 20px;
}
.required-note .required { color: #dc2626; }
/* Form footer */
.form-footer {
padding: 16px 24px; border-top: 1px solid #e5e5e3;
display: flex; justify-content: space-between; align-items: center;
background: #fafafa;
}
.form-footer-info { font-size: 12px; color: #999; }
.form-footer-actions { display: flex; gap: 8px; }
/* Success toast */
.toast {
position: fixed; bottom: 24px; right: 24px; background: #166534;
color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 13px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center;
gap: 8px; z-index: 100;
}
.toast-icon { font-size: 16px; }
*: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 active" href="#add" aria-current="page">
<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" href="#settings">
<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">
<div style="display: flex; align-items: center; gap: 12px;">
<a href="#inventory" class="btn-ghost" style="padding: 4px; border: none; background: none; cursor: pointer; color: #666; text-decoration: none;" aria-label="Volver al inventario">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
<h2>Agregar Dispositivo</h2>
</div>
<div class="topbar-actions">
<button class="btn">Cancelar</button>
<button class="btn btn-primary">Guardar Dispositivo</button>
</div>
</header>
<div class="content">
<p class="required-note">Los campos marcados con <span class="required">*</span> son obligatorios.</p>
<form class="form-card" aria-label="Formulario de nuevo dispositivo">
<!-- Identification -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Identificación</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="catalog_number">Número de Catálogo Interno <span class="required">*</span></label>
<input class="form-input" type="text" id="catalog_number" placeholder="INV-2024-0248" required aria-required="true">
<span class="form-hint">Se genera automáticamente si se deja vacío</span>
</div>
<div class="form-group">
<label class="form-label" for="serial_number">Número de Serie <span class="required">*</span></label>
<input class="form-input" type="text" id="serial_number" placeholder="C02X1234JHD5" required aria-required="true">
</div>
<div class="form-group">
<label class="form-label" for="service_tag">Service Tag</label>
<input class="form-input" type="text" id="service_tag" placeholder="ABC1234">
<span class="form-hint">Número de servicio del fabricante</span>
</div>
<div class="form-group">
<label class="form-label" for="additional_reg">Registro Adicional</label>
<input class="form-input" type="text" id="additional_reg" placeholder="Cualquier otro número de registro">
</div>
</div>
</div>
<!-- Device info -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Información del Dispositivo</div>
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label" for="device_name">Nombre / Descripción <span class="required">*</span></label>
<input class="form-input" type="text" id="device_name" placeholder="MacBook Pro 14" Oficina Dirección" required aria-required="true">
</div>
<div class="form-group">
<label class="form-label" for="device_type">Tipo de Dispositivo <span class="required">*</span></label>
<select class="form-select" id="device_type" required aria-required="true">
<option value="">Seleccionar tipo...</option>
<option>Ordenador de sobremesa</option>
<option selected>Portátil</option>
<option>Monitor / Pantalla</option>
<option>Impresora</option>
<option>Escáner</option>
<option>Dispositivo de red</option>
<option>Periférico</option>
<option>Servidor</option>
<option>SAI / Regulador</option>
<option>Almacenamiento externo</option>
<option>Otro</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="brand">Marca</label>
<input class="form-input" type="text" id="brand" placeholder="Apple">
</div>
<div class="form-group">
<label class="form-label" for="model">Modelo</label>
<input class="form-input" type="text" id="model" placeholder="MacBook Pro M3">
</div>
<div class="form-group">
<label class="form-label" for="serial_internal">N.º Serie Interno</label>
<input class="form-input" type="text" id="serial_internal" placeholder="Número interno de control">
</div>
<div class="form-group">
<label class="form-label" for="condition">Estado Físico</label>
<select class="form-select" id="condition">
<option value="excellent">Excelente</option>
<option value="good" selected>Bueno</option>
<option value="fair">Regular</option>
<option value="poor">Malo</option>
<option value="damaged">Dañado</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="status">Estado</label>
<select class="form-select" id="status">
<option value="active" selected>Activo</option>
<option value="available">Disponible</option>
<option value="reserved">Reservado</option>
</select>
</div>
</div>
</div>
<!-- Purchase info -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Compra y Garantía</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="purchase_date">Fecha de Compra</label>
<input class="form-input" type="date" id="purchase_date">
</div>
<div class="form-group">
<label class="form-label" for="warranty_expiry">Fin de Garantía</label>
<input class="form-input" type="date" id="warranty_expiry">
</div>
<div class="form-group">
<label class="form-label" for="purchase_price">Precio de Compra (€)</label>
<input class="form-input" type="number" id="purchase_price" placeholder="0.00" step="0.01" min="0">
</div>
</div>
</div>
<!-- Assignment -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Asignación</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="location">Ubicación Asignada</label>
<select class="form-select" id="location">
<option value="">Seleccionar ubicación...</option>
<option>Planta 0 — Recepción</option>
<option>Planta 1 — Open Space</option>
<option>Planta 1 — Reprografía</option>
<option>Planta 1 — Cuarto Redes</option>
<option>Planta 2 — Desp. Dirección</option>
<option>Almacén</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="assigned_user">Usuario Asignado</label>
<input class="form-input" type="text" id="assigned_user" placeholder="Nombre del usuario">
</div>
<div class="form-group">
<label class="form-label" for="department">Departamento</label>
<select class="form-select" id="department">
<option value="">Seleccionar departamento...</option>
<option>Informática</option>
<option>Administración</option>
<option>Recursos Humanos</option>
<option>Contabilidad</option>
<option>Dirección</option>
</select>
</div>
</div>
</div>
<!-- Notes -->
<div class="form-section" style="padding: 24px;">
<div class="form-section-title">Notas</div>
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label" for="notes">Observaciones</label>
<textarea class="form-textarea" id="notes" placeholder="Notas adicionales sobre el dispositivo..." rows="3"></textarea>
</div>
</div>
</div>
<!-- Footer -->
<div class="form-footer">
<span class="form-footer-info">Los cambios se guardan localmente</span>
<div class="form-footer-actions">
<button type="button" class="btn">Cancelar</button>
<button type="button" class="btn" style="border-color: #e5e5e3;">Guardar y Agregar Otro</button>
<button type="submit" class="btn btn-primary">Guardar Dispositivo</button>
</div>
</div>
</form>
</div>
</main>
<!-- Toast demo -->
<div class="toast" role="alert" aria-live="polite">
<span class="toast-icon" aria-hidden="true"></span>
Dispositivo INV-2024-0247 guardado correctamente
</div>
</body>
</html>