Primer commit — OPSV Dashboard de siniestralidad vial
This commit is contained in:
@@ -0,0 +1,888 @@
|
||||
<!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">2–3 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">3–4 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">3–4 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">2–3 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 2–3 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> |
|
||||
Arquitectura v2.0 · Abril 2026 |
|
||||
Stack: React + Supabase + Vercel
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user