Files
OPSV---Dashboard-de-Siniest…/opsv-arquitectura-v2 (1).html

888 lines
36 KiB
HTML
Raw Permalink 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 lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPSV — Plan de Arquitectura v2.0</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--brand: #252C61;
--brand-mid: #3A4490;
--brand-light: #E8EAF6;
--accent: #E63946;
--accent-soft: #FDECEA;
--green: #2D7A4F;
--green-soft: #E6F4EE;
--gold: #C9A84C;
--gold-soft: #FDF6E3;
--text: #1A1D2E;
--text-muted: #5A5F7A;
--text-faint: #9EA3BC;
--surface: #F7F8FC;
--surface-2: #FFFFFF;
--border: rgba(37,44,97,0.12);
--radius: 10px;
--radius-lg: 16px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Montserrat', sans-serif;
background: var(--surface);
color: var(--text);
line-height: 1.6;
font-size: 15px;
}
/* ─── HEADER ─────────────────────────────── */
.doc-header {
background: var(--brand);
color: white;
padding: 48px 40px 40px;
position: relative;
overflow: hidden;
}
.doc-header::before {
content: '';
position: absolute; top: 0; right: 0;
width: 400px; height: 100%;
background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.04) 100%);
}
.doc-header .badge {
display: inline-block;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
border-radius: 100px;
padding: 4px 14px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 16px;
}
.doc-header h1 {
font-size: 28px; font-weight: 800;
line-height: 1.2; margin-bottom: 10px;
}
.doc-header p {
color: rgba(255,255,255,0.7);
font-size: 14px; font-weight: 400;
max-width: 560px;
}
.doc-header .meta {
display: flex; gap: 24px;
margin-top: 24px;
}
.doc-header .meta span {
font-size: 12px; color: rgba(255,255,255,0.6);
font-weight: 500;
}
.doc-header .meta strong { color: white; }
/* ─── LAYOUT ──────────────────────────────── */
.doc-body {
max-width: 960px;
margin: 0 auto;
padding: 40px 24px 80px;
}
/* ─── SECTION TITLES ──────────────────────── */
.section-title {
display: flex; align-items: center; gap: 10px;
margin: 48px 0 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--brand-light);
}
.section-title .num {
width: 28px; height: 28px;
background: var(--brand); color: white;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700; flex-shrink: 0;
}
.section-title h2 {
font-size: 17px; font-weight: 700;
color: var(--brand);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ─── CARDS GRID ──────────────────────────── */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
margin-bottom: 8px;
}
.card {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute; top: 0; left: 0;
width: 4px; height: 100%;
background: var(--brand);
}
.card.green::before { background: var(--green); }
.card.gold::before { background: var(--gold); }
.card.red::before { background: var(--accent); }
.card .card-icon {
font-size: 22px; margin-bottom: 10px;
}
.card h3 {
font-size: 13px; font-weight: 700;
color: var(--brand); margin-bottom: 6px;
text-transform: uppercase; letter-spacing: 0.04em;
}
.card.green h3 { color: var(--green); }
.card.gold h3 { color: #8B6914; }
.card.red h3 { color: var(--accent); }
.card p {
font-size: 13px; color: var(--text-muted);
line-height: 1.55;
}
.card .tag {
display: inline-block; margin-top: 10px;
background: var(--brand-light); color: var(--brand);
border-radius: 100px; padding: 2px 10px;
font-size: 11px; font-weight: 600;
}
.card.green .tag { background: var(--green-soft); color: var(--green); }
.card.gold .tag { background: var(--gold-soft); color: #8B6914; }
.card.red .tag { background: var(--accent-soft); color: var(--accent); }
/* ─── STACK TABLE ──────────────────────────── */
.stack-table {
width: 100%; border-collapse: collapse;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 8px;
}
.stack-table th {
background: var(--brand);
color: white; font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.07em;
padding: 10px 16px; text-align: left;
}
.stack-table td {
padding: 12px 16px; font-size: 13px;
border-top: 1px solid var(--border);
vertical-align: top;
}
.stack-table tr:hover td { background: var(--brand-light); }
.stack-table .pill {
display: inline-block;
background: var(--brand-light); color: var(--brand);
border-radius: 100px; padding: 1px 8px;
font-size: 11px; font-weight: 600;
}
.stack-table .pill.free { background: var(--green-soft); color: var(--green); }
.stack-table td strong { color: var(--brand); font-weight: 600; }
.stack-table td .why { color: var(--text-muted); font-size: 12px; margin-top: 2px; }
/* ─── ARQUITECTURA SVG ─────────────────────── */
.arch-wrapper {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 32px 24px;
margin-bottom: 8px;
overflow-x: auto;
}
.arch-wrapper svg { max-width: 100%; display: block; margin: 0 auto; }
/* ─── ROLES TABLE ──────────────────────────── */
.roles-table {
width: 100%; border-collapse: collapse;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
font-size: 13px;
}
.roles-table th {
background: var(--brand); color: white;
padding: 10px 14px; text-align: left;
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.07em;
}
.roles-table td {
padding: 11px 14px;
border-top: 1px solid var(--border);
}
.roles-table tr:hover td { background: var(--brand-light); }
.check { color: var(--green); font-weight: 700; }
.cross { color: #CCC; font-weight: 700; }
.partial { color: var(--gold); font-weight: 700; }
.role-badge {
display: inline-block;
border-radius: 100px; padding: 2px 10px;
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
}
.rb-superadmin { background: #E8EAF6; color: #252C61; }
.rb-admin { background: #E6F4EE; color: #2D7A4F; }
.rb-editor { background: #FDF6E3; color: #8B6914; }
.rb-public { background: #F5F5F5; color: #555; }
/* ─── SCHEMA ───────────────────────────────── */
.schema-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 14px;
}
.schema-card {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.schema-card .schema-header {
background: var(--brand);
color: white; padding: 8px 14px;
font-size: 12px; font-weight: 700;
font-family: 'Source Code Pro', monospace;
letter-spacing: 0.03em;
}
.schema-card ul { list-style: none; padding: 10px 0; }
.schema-card li {
padding: 5px 14px;
font-family: 'Source Code Pro', monospace;
font-size: 12px; color: var(--text-muted);
display: flex; justify-content: space-between;
gap: 8px;
}
.schema-card li:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.schema-card li .col-name { color: var(--text); font-weight: 500; }
.schema-card li .col-type { color: var(--brand-mid); font-size: 11px; }
.schema-card li .pk { color: var(--gold); font-size: 10px; font-weight: 700; }
.schema-card li .fk { color: var(--green); font-size: 10px; font-weight: 700; }
/* ─── FOLDER TREE ──────────────────────────── */
.folder-tree {
background: #1A1D2E;
border-radius: var(--radius-lg);
padding: 24px;
font-family: 'Source Code Pro', monospace;
font-size: 13px;
line-height: 1.9;
color: #9EA3BC;
overflow-x: auto;
}
.folder-tree .dir { color: #7FDBFF; }
.folder-tree .file { color: #ADBAC7; }
.folder-tree .cmt { color: #5A5F7A; font-style: italic; }
.folder-tree .key { color: #E6DB74; }
/* ─── PHASES ───────────────────────────────── */
.phases { display: flex; flex-direction: column; gap: 12px; }
.phase-card {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
display: grid;
grid-template-columns: 80px 1fr;
}
.phase-num {
background: var(--brand);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 20px 10px;
color: white;
}
.phase-num .ph { font-size: 10px; font-weight: 600; opacity: 0.7; letter-spacing: 0.08em; text-transform: uppercase; }
.phase-num .n { font-size: 32px; font-weight: 800; line-height: 1; }
.phase-num .wk { font-size: 11px; opacity: 0.7; margin-top: 4px; }
.phase-content { padding: 20px 22px; }
.phase-content h3 { font-size: 15px; font-weight: 700; color: var(--brand); margin-bottom: 8px; }
.phase-tasks { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.phase-tasks li {
font-size: 13px; color: var(--text-muted);
padding-left: 16px; position: relative;
}
.phase-tasks li::before {
content: '→';
position: absolute; left: 0;
color: var(--brand); font-weight: 700;
}
.phase-outcome {
margin-top: 12px; padding: 8px 12px;
background: var(--brand-light);
border-radius: 6px;
font-size: 12px; color: var(--brand); font-weight: 600;
}
.phase-outcome::before { content: '✓ Entregable: '; }
/* ─── CALLOUT ──────────────────────────────── */
.callout {
background: var(--brand-light);
border-left: 4px solid var(--brand);
border-radius: 0 var(--radius) var(--radius) 0;
padding: 14px 18px;
margin-top: 12px;
font-size: 13px; color: var(--brand);
}
.callout strong { font-weight: 700; }
.callout.warning {
background: var(--gold-soft);
border-color: var(--gold);
color: #6B4F00;
}
/* ─── FOOTER ───────────────────────────────── */
.doc-footer {
background: var(--brand);
color: rgba(255,255,255,0.5);
text-align: center;
padding: 20px;
font-size: 12px;
margin-top: 60px;
}
.doc-footer strong { color: white; }
</style>
</head>
<body>
<!-- HEADER -->
<header class="doc-header">
<div class="badge">Especificación Técnica</div>
<h1>OPSV Dashboard v2.0<br>Plan de Arquitectura</h1>
<p>Documento de referencia para el desarrollo con Claude Code. Define stack tecnológico, estructura de base de datos, roles de usuario y fases de desarrollo.</p>
<div class="meta">
<span><strong>Versión</strong> 2.0</span>
<span><strong>Fecha</strong> Abril 2026</span>
<span><strong>Stack</strong> React + Supabase + Vercel</span>
<span><strong>Modalidad</strong> Solo developer · Claude Code</span>
</div>
</header>
<main class="doc-body">
<!-- 1. DECISIONES -->
<div class="section-title">
<div class="num">1</div>
<h2>Decisiones de arquitectura</h2>
</div>
<div class="cards-grid">
<div class="card">
<div class="card-icon">⚛️</div>
<h3>Frontend</h3>
<p>React + Vite — mismo entorno que usaste en el dashboard anterior. Claude Code lo maneja perfectamente.</p>
<span class="tag">React 18 + Vite 5</span>
</div>
<div class="card green">
<div class="card-icon">🗄️</div>
<h3>Backend + Auth</h3>
<p>Supabase: base de datos PostgreSQL, autenticación email/password, storage para los CSV. Todo gratuito.</p>
<span class="tag">Supabase Free Tier</span>
</div>
<div class="card gold">
<div class="card-icon">🚀</div>
<h3>Hosting</h3>
<p>Vercel: deploy automático desde GitHub, HTTPS incluido, dominio gratuito bajo vercel.app.</p>
<span class="tag">Vercel Hobby (gratis)</span>
</div>
<div class="card red">
<div class="card-icon">🎨</div>
<h3>UI + Gráficos</h3>
<p>Tailwind CSS para estilos. Recharts para visualizaciones (mismo que ya usás). Lucide para iconos.</p>
<span class="tag">Tailwind + Recharts</span>
</div>
</div>
<div class="callout">
<strong>¿Por qué no GitHub Pages?</strong> GitHub Pages es estático puro — no puede manejar autenticación ni guardar datos. Vercel sí, y también es 100% gratuito con deploy automático cada vez que hacés push.
</div>
<!-- 2. STACK DETALLE -->
<div class="section-title">
<div class="num">2</div>
<h2>Stack tecnológico completo</h2>
</div>
<table class="stack-table">
<thead>
<tr>
<th>Capa</th>
<th>Tecnología</th>
<th>Para qué sirve</th>
<th>Costo</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>UI Framework</strong></td>
<td>React 18 + Vite</td>
<td>
Interfaz del dashboard y panel admin
<div class="why">Familiar para Claude Code, ecosistema enorme</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Estilos</strong></td>
<td>Tailwind CSS v3</td>
<td>
Diseño responsivo, paleta institucional OPSV
<div class="why">Claude Code genera Tailwind excelente</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Gráficos</strong></td>
<td>Recharts</td>
<td>
Todos los gráficos del dashboard actual
<div class="why">Mismo que el dashboard anterior → migración directa</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Base de datos</strong></td>
<td>Supabase PostgreSQL</td>
<td>
Config del dashboard, textos editables, roles, metadatos de CSV
<div class="why">500MB gratis, más que suficiente para configuración</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Autenticación</strong></td>
<td>Supabase Auth</td>
<td>
Login email/contraseña con roles (superadmin, admin, editor)
<div class="why">Email confirmado + recupero de contraseña incluidos</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Storage CSV</strong></td>
<td>Supabase Storage</td>
<td>
Almacenamiento de los 3 archivos CSV mensuales
<div class="why">1GB gratis, historial de versiones posible</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Hosting</strong></td>
<td>Vercel</td>
<td>
Publicación del sitio, deploy automático desde GitHub
<div class="why">Cada push = nueva versión publicada en segundos</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
<tr>
<td><strong>Repositorio</strong></td>
<td>GitHub</td>
<td>
Control de versiones, integración con Vercel
<div class="why">Todo el código centralizado, historial completo</div>
</td>
<td><span class="pill free">Gratis</span></td>
</tr>
</tbody>
</table>
<!-- 3. ARQUITECTURA -->
<div class="section-title">
<div class="num">3</div>
<h2>Diagrama de arquitectura</h2>
</div>
<div class="arch-wrapper">
<svg viewBox="0 0 820 420" xmlns="http://www.w3.org/2000/svg" font-family="Montserrat, sans-serif">
<!-- Background zones -->
<rect x="10" y="10" width="240" height="400" rx="12" fill="#F7F8FC" stroke="#E8EAF6" stroke-width="1.5"/>
<rect x="290" y="10" width="240" height="400" rx="12" fill="#F7F8FC" stroke="#E8EAF6" stroke-width="1.5"/>
<rect x="570" y="10" width="240" height="400" rx="12" fill="#F7F8FC" stroke="#E8EAF6" stroke-width="1.5"/>
<!-- Zone labels -->
<text x="130" y="36" text-anchor="middle" font-size="10" font-weight="700" fill="#9EA3BC" letter-spacing="0.1em">USUARIOS</text>
<text x="410" y="36" text-anchor="middle" font-size="10" font-weight="700" fill="#9EA3BC" letter-spacing="0.1em">FRONTEND · VERCEL</text>
<text x="690" y="36" text-anchor="middle" font-size="10" font-weight="700" fill="#9EA3BC" letter-spacing="0.1em">BACKEND · SUPABASE</text>
<!-- USUARIO PÚBLICO -->
<rect x="30" y="55" width="200" height="72" rx="10" fill="white" stroke="#ddd" stroke-width="1.5"/>
<circle cx="58" cy="91" r="14" fill="#F5F5F5" stroke="#ddd"/>
<text x="58" y="96" text-anchor="middle" font-size="14">👤</text>
<text x="80" y="83" font-size="12" font-weight="700" fill="#333">Usuario Público</text>
<text x="80" y="100" font-size="11" fill="#888">Sin login requerido</text>
<rect x="80" y="108" width="120" height="13" rx="6" fill="#F5F5F5"/>
<text x="140" y="119" text-anchor="middle" font-size="10" fill="#888">Acceso al dashboard</text>
<!-- ADMIN -->
<rect x="30" y="150" width="200" height="90" rx="10" fill="white" stroke="#252C61" stroke-width="1.5"/>
<circle cx="58" cy="195" r="14" fill="#E8EAF6" stroke="#252C61"/>
<text x="58" y="200" text-anchor="middle" font-size="14">🔐</text>
<text x="80" y="183" font-size="12" font-weight="700" fill="#252C61">Administrador</text>
<text x="80" y="200" font-size="11" fill="#5A5F7A">Login email/password</text>
<rect x="80" y="208" width="108" height="13" rx="6" fill="#E8EAF6"/>
<text x="134" y="219" text-anchor="middle" font-size="10" fill="#252C61">superadmin | admin</text>
<rect x="80" y="225" width="80" height="13" rx="6" fill="#E6F4EE"/>
<text x="120" y="236" text-anchor="middle" font-size="10" fill="#2D7A4F">editor</text>
<!-- ARROWS user → frontend -->
<line x1="230" y1="91" x2="290" y2="91" stroke="#ccc" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="287,87 295,91 287,95" fill="#ccc"/>
<line x1="230" y1="195" x2="290" y2="195" stroke="#252C61" stroke-width="1.5"/>
<polygon points="287,191 295,195 287,199" fill="#252C61"/>
<!-- FRONTEND BOXES -->
<!-- Dashboard público -->
<rect x="305" y="55" width="210" height="60" rx="10" fill="white" stroke="#ddd" stroke-width="1.5"/>
<text x="320" y="80" font-size="12" font-weight="700" fill="#333">Dashboard Público</text>
<text x="320" y="97" font-size="11" fill="#888">/ · sin autenticación</text>
<rect x="390" y="103" width="90" height="8" rx="4" fill="#F0F0F0"/>
<text x="435" y="110" text-anchor="middle" font-size="9" fill="#999">React + Recharts</text>
<!-- Panel Admin -->
<rect x="305" y="135" width="210" height="80" rx="10" fill="white" stroke="#252C61" stroke-width="1.5"/>
<text x="320" y="158" font-size="12" font-weight="700" fill="#252C61">Panel Admin</text>
<text x="320" y="175" font-size="11" fill="#5A5F7A">/admin · ruta protegida</text>
<text x="320" y="193" font-size="11" fill="#5A5F7A">Upload CSV · Editar textos</text>
<rect x="320" y="200" width="128" height="9" rx="4" fill="#E8EAF6"/>
<text x="384" y="208" text-anchor="middle" font-size="9" fill="#252C61">React Router + Auth guard</text>
<!-- Login -->
<rect x="305" y="235" width="210" height="50" rx="10" fill="white" stroke="#ddd" stroke-width="1.5"/>
<text x="320" y="256" font-size="12" font-weight="700" fill="#333">Login / Auth</text>
<text x="320" y="273" font-size="11" fill="#888">/login · email + password</text>
<!-- GitHub / Deploy -->
<rect x="305" y="305" width="210" height="90" rx="10" fill="#1A1D2E" stroke="#333" stroke-width="1"/>
<text x="320" y="328" font-size="11" font-weight="700" fill="#7FDBFF">GitHub Repository</text>
<text x="320" y="346" font-size="10" fill="#9EA3BC">Push → Vercel auto-deploy</text>
<text x="320" y="364" font-size="10" fill="#9EA3BC">Variables de entorno Supabase</text>
<text x="320" y="382" font-size="10" fill="#9EA3BC">en Vercel (VITE_SUPABASE_URL)</text>
<!-- ARROWS frontend → backend -->
<line x1="515" y1="85" x2="570" y2="85" stroke="#ccc" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="567,81 575,85 567,89" fill="#ccc"/>
<line x1="515" y1="175" x2="570" y2="175" stroke="#252C61" stroke-width="1.5"/>
<polygon points="567,171 575,175 567,179" fill="#252C61"/>
<line x1="515" y1="260" x2="570" y2="260" stroke="#2D7A4F" stroke-width="1.5"/>
<polygon points="567,256 575,260 567,264" fill="#2D7A4F"/>
<!-- SUPABASE boxes -->
<!-- Auth -->
<rect x="585" y="55" width="210" height="60" rx="10" fill="white" stroke="#2D7A4F" stroke-width="1.5"/>
<text x="600" y="78" font-size="12" font-weight="700" fill="#2D7A4F">Supabase Auth</text>
<text x="600" y="95" font-size="11" fill="#5A5F7A">JWT · Roles · Email confirm</text>
<rect x="600" y="102" width="70" height="8" rx="4" fill="#E6F4EE"/>
<text x="635" y="109" text-anchor="middle" font-size="9" fill="#2D7A4F">user_roles table</text>
<!-- DB -->
<rect x="585" y="135" width="210" height="90" rx="10" fill="white" stroke="#252C61" stroke-width="1.5"/>
<text x="600" y="157" font-size="12" font-weight="700" fill="#252C61">PostgreSQL DB</text>
<text x="600" y="174" font-size="11" fill="#5A5F7A">dashboard_config</text>
<text x="600" y="191" font-size="11" fill="#5A5F7A">user_roles · datasets</text>
<text x="600" y="208" font-size="11" fill="#5A5F7A">Textos · Chart config</text>
<rect x="600" y="214" width="55" height="8" rx="4" fill="#E8EAF6"/>
<text x="627" y="221" text-anchor="middle" font-size="9" fill="#252C61">Row Level Security</text>
<!-- Storage -->
<rect x="585" y="245" width="210" height="70" rx="10" fill="white" stroke="#C9A84C" stroke-width="1.5"/>
<text x="600" y="268" font-size="12" font-weight="700" fill="#8B6914">Supabase Storage</text>
<text x="600" y="285" font-size="11" fill="#5A5F7A">siniestros.csv</text>
<text x="600" y="302" font-size="11" fill="#5A5F7A">personas.csv · involucrados.csv</text>
<!-- API label -->
<text x="543" y="130" text-anchor="middle" font-size="10" fill="#9EA3BC" transform="rotate(-90 543 130)">REST API</text>
<text x="543" y="220" text-anchor="middle" font-size="10" fill="#9EA3BC" transform="rotate(-90 543 220)">SDK Client</text>
<!-- Bottom label -->
<text x="690" y="340" text-anchor="middle" font-size="11" fill="#C9A84C" font-weight="600">Free tier · 500MB DB + 1GB Storage</text>
<text x="690" y="358" text-anchor="middle" font-size="11" fill="#9EA3BC">Respaldo automático · SSL incluido</text>
</svg>
</div>
<!-- 4. ROLES -->
<div class="section-title">
<div class="num">4</div>
<h2>Roles de usuario y permisos</h2>
</div>
<table class="roles-table">
<thead>
<tr>
<th>Rol</th>
<th>Ver dashboard</th>
<th>Subir CSV</th>
<th>Editar textos/resumen</th>
<th>Config gráficos</th>
<th>Gestionar usuarios</th>
<th>Acceso</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="role-badge rb-superadmin">Superadmin</span></td>
<td class="check"></td>
<td class="check"></td>
<td class="check"></td>
<td class="check"></td>
<td class="check">✓ Crear/borrar admins</td>
<td>Login requerido</td>
</tr>
<tr>
<td><span class="role-badge rb-admin">Admin</span></td>
<td class="check"></td>
<td class="check"></td>
<td class="check"></td>
<td class="check"></td>
<td class="cross"></td>
<td>Login requerido</td>
</tr>
<tr>
<td><span class="role-badge rb-editor">Editor</span></td>
<td class="check"></td>
<td class="cross"></td>
<td class="check">✓ Solo textos</td>
<td class="cross"></td>
<td class="cross"></td>
<td>Login requerido</td>
</tr>
<tr>
<td><span class="role-badge rb-public">Público</span></td>
<td class="check">✓ Solo lectura</td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
<td>Sin login · acceso libre</td>
</tr>
</tbody>
</table>
<!-- 5. ESQUEMA DB -->
<div class="section-title">
<div class="num">5</div>
<h2>Esquema de base de datos (Supabase)</h2>
</div>
<div class="schema-grid">
<div class="schema-card">
<div class="schema-header">user_roles</div>
<ul>
<li><span class="col-name">id</span><span class="col-type">uuid <span class="pk">PK</span></span></li>
<li><span class="col-name">user_id</span><span class="col-type">uuid <span class="fk">FK auth</span></span></li>
<li><span class="col-name">role</span><span class="col-type">enum</span></li>
<li><span class="col-name">created_at</span><span class="col-type">timestamp</span></li>
</ul>
</div>
<div class="schema-card">
<div class="schema-header">datasets</div>
<ul>
<li><span class="col-name">id</span><span class="col-type">uuid <span class="pk">PK</span></span></li>
<li><span class="col-name">type</span><span class="col-type">text</span></li>
<li><span class="col-name">year</span><span class="col-type">integer</span></li>
<li><span class="col-name">month</span><span class="col-type">integer</span></li>
<li><span class="col-name">storage_path</span><span class="col-type">text</span></li>
<li><span class="col-name">uploaded_by</span><span class="col-type">uuid <span class="fk">FK</span></span></li>
<li><span class="col-name">created_at</span><span class="col-type">timestamp</span></li>
</ul>
</div>
<div class="schema-card">
<div class="schema-header">dashboard_config</div>
<ul>
<li><span class="col-name">id</span><span class="col-type">uuid <span class="pk">PK</span></span></li>
<li><span class="col-name">key</span><span class="col-type">text unique</span></li>
<li><span class="col-name">value</span><span class="col-type">jsonb</span></li>
<li><span class="col-name">updated_by</span><span class="col-type">uuid <span class="fk">FK</span></span></li>
<li><span class="col-name">updated_at</span><span class="col-type">timestamp</span></li>
</ul>
</div>
<div class="schema-card">
<div class="schema-header">resumen_content</div>
<ul>
<li><span class="col-name">id</span><span class="col-type">uuid <span class="pk">PK</span></span></li>
<li><span class="col-name">year</span><span class="col-type">integer</span></li>
<li><span class="col-name">month</span><span class="col-type">integer</span></li>
<li><span class="col-name">title</span><span class="col-type">text</span></li>
<li><span class="col-name">body</span><span class="col-type">text (md)</span></li>
<li><span class="col-name">published</span><span class="col-type">boolean</span></li>
<li><span class="col-name">created_by</span><span class="col-type">uuid <span class="fk">FK</span></span></li>
</ul>
</div>
<div class="schema-card">
<div class="schema-header">chart_config</div>
<ul>
<li><span class="col-name">id</span><span class="col-type">uuid <span class="pk">PK</span></span></li>
<li><span class="col-name">chart_key</span><span class="col-type">text unique</span></li>
<li><span class="col-name">title</span><span class="col-type">text</span></li>
<li><span class="col-name">chart_type</span><span class="col-type">enum</span></li>
<li><span class="col-name">color_palette</span><span class="col-type">jsonb</span></li>
<li><span class="col-name">visible</span><span class="col-type">boolean</span></li>
<li><span class="col-name">updated_at</span><span class="col-type">timestamp</span></li>
</ul>
</div>
</div>
<div class="callout">
<strong>Row Level Security (RLS)</strong> — Supabase protege cada tabla con políticas: el público solo puede leer, los admins pueden escribir. Esto se configura una vez con SQL y funciona automáticamente.
</div>
<!-- 6. ESTRUCTURA DE CARPETAS -->
<div class="section-title">
<div class="num">6</div>
<h2>Estructura del proyecto</h2>
</div>
<div class="folder-tree">
<span class="dir">opsv-dashboard/</span>
├── <span class="dir">src/</span>
│ ├── <span class="dir">components/</span> <span class="cmt"># Componentes reutilizables</span>
│ │ ├── <span class="file">charts/</span> <span class="cmt"># BarChart, LineChart, etc.</span>
│ │ ├── <span class="file">ui/</span> <span class="cmt"># Button, Card, Modal, Input</span>
│ │ └── <span class="file">layout/</span> <span class="cmt"># Navbar, Sidebar, Footer</span>
│ │
│ ├── <span class="dir">pages/</span> <span class="cmt"># Páginas / rutas</span>
│ │ ├── <span class="file">Dashboard.jsx</span> <span class="cmt"># / → público</span>
│ │ ├── <span class="file">Login.jsx</span> <span class="cmt"># /login</span>
│ │ └── <span class="dir">admin/</span>
│ │ ├── <span class="file">AdminPanel.jsx</span> <span class="cmt"># /admin → panel principal</span>
│ │ ├── <span class="file">UploadCSV.jsx</span> <span class="cmt"># /admin/upload</span>
│ │ ├── <span class="file">EditResumen.jsx</span> <span class="cmt"># /admin/resumen</span>
│ │ ├── <span class="file">EditCharts.jsx</span> <span class="cmt"># /admin/charts</span>
│ │ └── <span class="file">ManageUsers.jsx</span> <span class="cmt"># /admin/users (superadmin)</span>
│ │
│ ├── <span class="dir">hooks/</span> <span class="cmt"># Custom hooks</span>
│ │ ├── <span class="file">useAuth.js</span> <span class="cmt"># Estado de sesión Supabase</span>
│ │ ├── <span class="file">useCSVData.js</span> <span class="cmt"># Carga y parseo de CSV</span>
│ │ └── <span class="file">useDashConfig.js</span> <span class="cmt"># Config editable desde DB</span>
│ │
│ ├── <span class="dir">lib/</span>
│ │ └── <span class="file">supabase.js</span> <span class="cmt"># Cliente Supabase (1 línea)</span>
│ │
│ ├── <span class="dir">utils/</span>
│ │ └── <span class="file">parseCSV.js</span> <span class="cmt"># Lógica de procesamiento CSV</span>
│ │
│ └── <span class="file">App.jsx</span> <span class="cmt"># Rutas + Auth provider</span>
├── <span class="file">.env.local</span> <span class="cmt"># VITE_SUPABASE_URL + ANON_KEY (no subir a GitHub)</span>
├── <span class="file">.env.example</span> <span class="cmt"># Template sin valores reales (sí subir)</span>
├── <span class="file">vercel.json</span> <span class="cmt"># Configuración de Vercel (SPA redirect)</span>
├── <span class="file">tailwind.config.js</span> <span class="cmt"># Paleta institucional OPSV #252C61</span>
└── <span class="file">vite.config.js</span>
</div>
<!-- 7. FASES -->
<div class="section-title">
<div class="num">7</div>
<h2>Fases de desarrollo</h2>
</div>
<div class="phases">
<div class="phase-card">
<div class="phase-num">
<span class="ph">Fase</span>
<span class="n">1</span>
<span class="wk">23 sem</span>
</div>
<div class="phase-content">
<h3>Fundación del proyecto</h3>
<ul class="phase-tasks">
<li>Crear proyecto en Supabase (auth + storage + DB)</li>
<li>Scaffold React + Vite con Tailwind y paleta OPSV</li>
<li>Sistema de autenticación: login, logout, sesión persistente</li>
<li>Ruta protegida <code>/admin</code> con auth guard</li>
<li>Conectar Vercel ↔ GitHub (deploy automático)</li>
</ul>
<div class="phase-outcome">Sitio online vacío con login funcional y deploy automático</div>
</div>
</div>
<div class="phase-card">
<div class="phase-num">
<div class="ph">Fase</div>
<div class="n">2</div>
<div class="wk">34 sem</div>
</div>
<div class="phase-content">
<h3>Dashboard público</h3>
<ul class="phase-tasks">
<li>Upload manual de los 3 CSV desde panel admin</li>
<li>Parser CSV en el cliente (Papa Parse)</li>
<li>Migrar todos los gráficos actuales (Recharts)</li>
<li>Filtros por año y mes funcionales</li>
<li>Sección Resumen con contenido desde DB</li>
<li>Diseño responsivo institucional</li>
</ul>
<div class="phase-outcome">Dashboard público funcional idéntico al actual, pero con datos dinámicos</div>
</div>
</div>
<div class="phase-card">
<div class="phase-num">
<div class="ph">Fase</div>
<div class="n">3</div>
<div class="wk">34 sem</div>
</div>
<div class="phase-content">
<h3>Panel administrador</h3>
<ul class="phase-tasks">
<li>Subida de CSV mensuales (3 archivos a la vez)</li>
<li>Editor de texto enriquecido para sección Resumen</li>
<li>Editor de títulos y configuración de gráficos</li>
<li>Selector de tipo de gráfico (barra/línea/área) por visualización</li>
<li>Selector de colores con paleta institucional</li>
<li>Vista previa antes de publicar</li>
</ul>
<div class="phase-outcome">Panel admin completo con edición en tiempo real y previsualización</div>
</div>
</div>
<div class="phase-card">
<div class="phase-num">
<div class="ph">Fase</div>
<div class="n">4</div>
<div class="wk">23 sem</div>
</div>
<div class="phase-content">
<h3>Escalabilidad y pulido</h3>
<ul class="phase-tasks">
<li>Gestión de múltiples usuarios con roles (admin, editor)</li>
<li>Historial de versiones de CSV por mes/año</li>
<li>Exportación PDF desde el panel admin</li>
<li>Notificaciones de carga exitosa / errores</li>
<li>Pruebas finales y documentación básica</li>
</ul>
<div class="phase-outcome">Sistema production-ready con multi-usuario y gestión de versiones</div>
</div>
</div>
</div>
<div class="callout warning">
<strong>Estimación realista para una persona con Claude Code:</strong> Fases 1 + 2 son las más importantes y deberían estar listas en ~6 semanas trabajando 23 horas/día. Las fases 3 y 4 pueden ir en paralelo con el uso real del sistema.
</div>
</main>
<footer class="doc-footer">
<strong>OPSV — Observatorio Provincial de Seguridad Vial · Santa Cruz</strong> &nbsp;|&nbsp;
Arquitectura v2.0 · Abril 2026 &nbsp;|&nbsp;
Stack: React + Supabase + Vercel
</footer>
</body>
</html>