init: rapid_dashboards app from fn_registry
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
# Ejemplos de dashboards
|
||||
|
||||
## 1. Dashboard minimo — una BD, un KPI
|
||||
|
||||
El dashboard mas simple posible: una conexion SQLite y un numero.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Contador"
|
||||
refresh: 5s
|
||||
columns: 4
|
||||
|
||||
theme: "dark"
|
||||
|
||||
connections:
|
||||
db:
|
||||
driver: sqlite
|
||||
path: ./data.db
|
||||
|
||||
queries:
|
||||
total:
|
||||
sql: "SELECT COUNT(*) as value FROM items"
|
||||
connection: db
|
||||
|
||||
filters: {}
|
||||
|
||||
sections:
|
||||
- id: main
|
||||
title: "Total"
|
||||
widgets:
|
||||
- id: count
|
||||
type: kpi
|
||||
title: "Items"
|
||||
query: total
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dashboard con filtros — Postgres + select + date range
|
||||
|
||||
Dashboard de ventas con filtros interactivos que re-ejecutan las queries.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Sales Dashboard"
|
||||
refresh: 30s
|
||||
columns: 12
|
||||
|
||||
theme: "dark"
|
||||
|
||||
connections:
|
||||
sales:
|
||||
driver: postgres
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: analytics
|
||||
password: "${SALES_PW}"
|
||||
database: sales
|
||||
|
||||
queries:
|
||||
revenue_kpi:
|
||||
connection: sales
|
||||
sql: |
|
||||
SELECT SUM(amount) as value, COUNT(*) as orders
|
||||
FROM orders
|
||||
WHERE created_at >= :from AND created_at <= :to
|
||||
AND (:cat = 'all' OR category = :cat)
|
||||
refresh: 15s
|
||||
params:
|
||||
from: "$filter.periodo.from"
|
||||
to: "$filter.periodo.to"
|
||||
cat: "$filter.categoria"
|
||||
|
||||
revenue_daily:
|
||||
connection: sales
|
||||
sql: |
|
||||
SELECT date_trunc('day', created_at)::date as day,
|
||||
SUM(amount) as revenue
|
||||
FROM orders
|
||||
WHERE created_at >= :from
|
||||
AND (:cat = 'all' OR category = :cat)
|
||||
GROUP BY day ORDER BY day
|
||||
refresh: 30s
|
||||
params:
|
||||
from: "$filter.periodo.from"
|
||||
cat: "$filter.categoria"
|
||||
|
||||
top_categories:
|
||||
connection: sales
|
||||
sql: |
|
||||
SELECT category, SUM(amount) as revenue
|
||||
FROM orders
|
||||
WHERE created_at >= :from
|
||||
GROUP BY category ORDER BY revenue DESC LIMIT 10
|
||||
refresh: 30s
|
||||
params:
|
||||
from: "$filter.periodo.from"
|
||||
|
||||
recent_orders:
|
||||
connection: sales
|
||||
sql: |
|
||||
SELECT id, customer, amount, category, status, created_at
|
||||
FROM orders ORDER BY created_at DESC LIMIT 25
|
||||
refresh: 10s
|
||||
|
||||
filters:
|
||||
periodo:
|
||||
type: date_range
|
||||
label: "Periodo"
|
||||
default: { from: "now-7d", to: "now" }
|
||||
presets:
|
||||
- { label: "Hoy", from: "now-0d", to: "now" }
|
||||
- { label: "7d", from: "now-7d", to: "now" }
|
||||
- { label: "30d", from: "now-30d", to: "now" }
|
||||
- { label: "90d", from: "now-90d", to: "now" }
|
||||
|
||||
categoria:
|
||||
type: select
|
||||
label: "Categoria"
|
||||
default: "all"
|
||||
options:
|
||||
- { label: "Todas", value: "all" }
|
||||
- { label: "Electronics", value: "electronics" }
|
||||
- { label: "Clothing", value: "clothing" }
|
||||
- { label: "Food", value: "food" }
|
||||
|
||||
sections:
|
||||
- id: kpis
|
||||
title: "Resumen"
|
||||
widgets:
|
||||
- id: revenue
|
||||
type: kpi
|
||||
title: "Revenue"
|
||||
query: revenue_kpi
|
||||
mapping: { value: "value", format: "$,.2f" }
|
||||
span: 4
|
||||
|
||||
- id: orders
|
||||
type: kpi
|
||||
title: "Ordenes"
|
||||
query: revenue_kpi
|
||||
mapping: { value: "orders", format: "," }
|
||||
span: 4
|
||||
|
||||
- id: charts
|
||||
title: "Tendencias"
|
||||
columns: 2
|
||||
widgets:
|
||||
- id: revenue_line
|
||||
type: line_chart
|
||||
title: "Revenue diario"
|
||||
query: revenue_daily
|
||||
mapping:
|
||||
x: "day"
|
||||
series: [{ key: "revenue", name: "Revenue" }]
|
||||
options: { curve: monotone, zoomable: true }
|
||||
span: 1
|
||||
|
||||
- id: categories_bar
|
||||
type: bar_chart
|
||||
title: "Top categorias"
|
||||
query: top_categories
|
||||
mapping: { x: "category", y: "revenue" }
|
||||
span: 1
|
||||
|
||||
- id: detail
|
||||
title: "Ordenes recientes"
|
||||
collapsible: true
|
||||
widgets:
|
||||
- id: orders_table
|
||||
type: table
|
||||
title: "Ultimas 25"
|
||||
query: recent_orders
|
||||
mapping:
|
||||
columns:
|
||||
- { key: "id", label: "ID" }
|
||||
- { key: "customer", label: "Cliente" }
|
||||
- { key: "amount", label: "Monto", format: "$,.2f" }
|
||||
- { key: "category", label: "Categoria" }
|
||||
- { key: "status", label: "Estado" }
|
||||
- { key: "created_at", label: "Fecha", format: "datetime" }
|
||||
span: 12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard multi-BD — SQLite + DuckDB
|
||||
|
||||
Combina datos de distintas bases en un solo dashboard.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Multi-DB Analytics"
|
||||
refresh: 10s
|
||||
columns: 12
|
||||
|
||||
theme: "dark"
|
||||
|
||||
connections:
|
||||
ops:
|
||||
driver: sqlite
|
||||
path: ./operations.db
|
||||
warehouse:
|
||||
driver: duckdb
|
||||
path: ./analytics.duckdb
|
||||
|
||||
queries:
|
||||
entities_count:
|
||||
connection: ops
|
||||
sql: "SELECT COUNT(*) as value FROM entities"
|
||||
refresh: 5s
|
||||
|
||||
executions_count:
|
||||
connection: ops
|
||||
sql: "SELECT COUNT(*) as value FROM executions"
|
||||
refresh: 5s
|
||||
|
||||
assertions_pass_rate:
|
||||
connection: ops
|
||||
sql: |
|
||||
SELECT
|
||||
ROUND(100.0 * SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) / COUNT(*), 1) as value
|
||||
FROM assertion_results
|
||||
refresh: 10s
|
||||
|
||||
daily_events:
|
||||
connection: warehouse
|
||||
sql: |
|
||||
SELECT date_trunc('day', ts) as day, COUNT(*) as events
|
||||
FROM events GROUP BY day ORDER BY day DESC LIMIT 30
|
||||
refresh: 30s
|
||||
|
||||
status_distribution:
|
||||
connection: ops
|
||||
sql: "SELECT status, COUNT(*) as count FROM entities GROUP BY status"
|
||||
refresh: 10s
|
||||
|
||||
filters: {}
|
||||
|
||||
sections:
|
||||
- id: kpis
|
||||
title: "Operations"
|
||||
widgets:
|
||||
- id: entities
|
||||
type: kpi
|
||||
title: "Entities"
|
||||
query: entities_count
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: executions
|
||||
type: kpi
|
||||
title: "Executions"
|
||||
query: executions_count
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: pass_rate
|
||||
type: kpi
|
||||
title: "Assertion Pass %"
|
||||
query: assertions_pass_rate
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: charts
|
||||
title: "Visualizacion"
|
||||
columns: 2
|
||||
widgets:
|
||||
- id: events_area
|
||||
type: area_chart
|
||||
title: "Eventos diarios (DuckDB)"
|
||||
query: daily_events
|
||||
mapping:
|
||||
x: "day"
|
||||
series: [{ key: "events", name: "Eventos" }]
|
||||
options: { show_grid: true }
|
||||
span: 1
|
||||
|
||||
- id: status_bar
|
||||
type: bar_chart
|
||||
title: "Distribucion por status (SQLite)"
|
||||
query: status_distribution
|
||||
mapping: { x: "status", y: "count" }
|
||||
span: 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Dashboard tiempo real — refresh sub-segundo
|
||||
|
||||
Para metricas que cambian rapido (CPU, colas, precios).
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Realtime Monitor"
|
||||
refresh: 1s
|
||||
columns: 12
|
||||
|
||||
theme: "dark"
|
||||
|
||||
connections:
|
||||
metrics:
|
||||
driver: sqlite
|
||||
path: ./metrics.db
|
||||
|
||||
queries:
|
||||
cpu_current:
|
||||
connection: metrics
|
||||
sql: "SELECT value FROM metrics WHERE key = 'cpu' ORDER BY ts DESC LIMIT 1"
|
||||
refresh: 200ms
|
||||
stale_time: 100ms
|
||||
|
||||
memory_current:
|
||||
connection: metrics
|
||||
sql: "SELECT value FROM metrics WHERE key = 'memory' ORDER BY ts DESC LIMIT 1"
|
||||
refresh: 500ms
|
||||
stale_time: 250ms
|
||||
|
||||
cpu_history:
|
||||
connection: metrics
|
||||
sql: |
|
||||
SELECT ts, value FROM metrics
|
||||
WHERE key = 'cpu' ORDER BY ts DESC LIMIT 60
|
||||
refresh: 1s
|
||||
|
||||
queue_depth:
|
||||
connection: metrics
|
||||
sql: "SELECT value FROM metrics WHERE key = 'queue' ORDER BY ts DESC LIMIT 1"
|
||||
refresh: 300ms
|
||||
stale_time: 150ms
|
||||
|
||||
filters: {}
|
||||
|
||||
sections:
|
||||
- id: live
|
||||
title: "Live"
|
||||
widgets:
|
||||
- id: cpu
|
||||
type: kpi
|
||||
title: "CPU %"
|
||||
query: cpu_current
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: mem
|
||||
type: kpi
|
||||
title: "Memory %"
|
||||
query: memory_current
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: queue
|
||||
type: kpi
|
||||
title: "Queue"
|
||||
query: queue_depth
|
||||
mapping: { value: "value" }
|
||||
span: 4
|
||||
|
||||
- id: history
|
||||
title: "CPU History (60s)"
|
||||
widgets:
|
||||
- id: cpu_line
|
||||
type: line_chart
|
||||
title: "CPU % (ultimo minuto)"
|
||||
query: cpu_history
|
||||
mapping:
|
||||
x: "ts"
|
||||
y: "value"
|
||||
options: { curve: linear, show_grid: true, height: 250 }
|
||||
span: 12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Dashboard con busqueda — filtro texto
|
||||
|
||||
Dashboard que permite buscar texto con debounce.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Buscador de funciones"
|
||||
refresh: 10s
|
||||
columns: 12
|
||||
|
||||
theme: "dark"
|
||||
|
||||
connections:
|
||||
registry:
|
||||
driver: sqlite
|
||||
path: ../../../registry.db
|
||||
|
||||
queries:
|
||||
search_results:
|
||||
connection: registry
|
||||
sql: |
|
||||
SELECT id, kind, lang, domain, description
|
||||
FROM functions
|
||||
WHERE id LIKE '%' || :q || '%'
|
||||
OR description LIKE '%' || :q || '%'
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 50
|
||||
refresh: 10s
|
||||
params:
|
||||
q: "$filter.busqueda"
|
||||
|
||||
result_count:
|
||||
connection: registry
|
||||
sql: |
|
||||
SELECT COUNT(*) as value
|
||||
FROM functions
|
||||
WHERE id LIKE '%' || :q || '%'
|
||||
OR description LIKE '%' || :q || '%'
|
||||
refresh: 10s
|
||||
params:
|
||||
q: "$filter.busqueda"
|
||||
|
||||
filters:
|
||||
busqueda:
|
||||
type: text
|
||||
label: "Buscar"
|
||||
default: ""
|
||||
placeholder: "nombre o descripcion..."
|
||||
debounce: 300
|
||||
|
||||
sections:
|
||||
- id: results
|
||||
title: "Resultados"
|
||||
widgets:
|
||||
- id: count
|
||||
type: kpi
|
||||
title: "Coincidencias"
|
||||
query: result_count
|
||||
mapping: { value: "value" }
|
||||
span: 3
|
||||
|
||||
- id: results_table
|
||||
type: table
|
||||
title: "Funciones encontradas"
|
||||
query: search_results
|
||||
mapping:
|
||||
columns:
|
||||
- { key: "id", label: "ID" }
|
||||
- { key: "kind", label: "Kind" }
|
||||
- { key: "lang", label: "Lang" }
|
||||
- { key: "domain", label: "Domain" }
|
||||
- { key: "description", label: "Descripcion" }
|
||||
span: 12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrones comunes
|
||||
|
||||
### Una query alimenta varios widgets
|
||||
|
||||
```yaml
|
||||
queries:
|
||||
summary:
|
||||
sql: "SELECT SUM(amount) as revenue, COUNT(*) as orders, AVG(amount) as avg FROM orders"
|
||||
|
||||
sections:
|
||||
- id: kpis
|
||||
title: "KPIs"
|
||||
widgets:
|
||||
- id: rev
|
||||
type: kpi
|
||||
query: summary
|
||||
mapping: { value: "revenue", format: "$,.2f" }
|
||||
span: 4
|
||||
- id: ord
|
||||
type: kpi
|
||||
query: summary
|
||||
mapping: { value: "orders", format: "," }
|
||||
span: 4
|
||||
- id: avg
|
||||
type: kpi
|
||||
query: summary
|
||||
mapping: { value: "avg", format: "$,.2f" }
|
||||
span: 4
|
||||
```
|
||||
|
||||
La query se ejecuta una sola vez y los 3 KPIs leen del mismo resultado.
|
||||
|
||||
### Seccion colapsable para detalle
|
||||
|
||||
```yaml
|
||||
sections:
|
||||
- id: detail
|
||||
title: "Detalle (click para expandir)"
|
||||
collapsible: true
|
||||
widgets:
|
||||
- id: log_table
|
||||
type: table
|
||||
query: logs
|
||||
span: 12
|
||||
```
|
||||
|
||||
### Grid flexible por seccion
|
||||
|
||||
```yaml
|
||||
sections:
|
||||
- id: kpis
|
||||
title: "KPIs"
|
||||
# hereda columns: 12 del global → 4 KPIs de span 3
|
||||
widgets:
|
||||
- { id: a, type: kpi, query: q, mapping: { value: v }, span: 3 }
|
||||
- { id: b, type: kpi, query: q, mapping: { value: v }, span: 3 }
|
||||
- { id: c, type: kpi, query: q, mapping: { value: v }, span: 3 }
|
||||
- { id: d, type: kpi, query: q, mapping: { value: v }, span: 3 }
|
||||
|
||||
- id: charts
|
||||
title: "Graficos"
|
||||
columns: 2 # override: solo 2 columnas
|
||||
widgets:
|
||||
- { id: chart1, type: line_chart, query: q1, mapping: { x: a, y: b }, span: 1 }
|
||||
- { id: chart2, type: bar_chart, query: q2, mapping: { x: a, y: b }, span: 1 }
|
||||
|
||||
- id: full
|
||||
title: "Tabla"
|
||||
columns: 1 # 1 columna = ancho completo
|
||||
widgets:
|
||||
- { id: tbl, type: table, query: q3, span: 1 }
|
||||
```
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
# Rapid Dashboards — Guia de uso
|
||||
|
||||
Dashboard builder declarativo: defines un YAML, obtienes un dashboard desktop con datos en vivo.
|
||||
|
||||
## Inicio rapido
|
||||
|
||||
```bash
|
||||
cd apps/rapid_dashboards
|
||||
|
||||
# Desarrollo — usa symlink dashboard.yaml (ver "Cambiar entre dashboards")
|
||||
wails dev
|
||||
|
||||
# Desarrollo — build produccion
|
||||
CGO_ENABLED=1 wails build -tags fts5
|
||||
./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml
|
||||
```
|
||||
|
||||
## Compilar un dashboard especifico
|
||||
|
||||
### Desarrollo (wails dev)
|
||||
|
||||
`wails dev` no pasa argumentos CLI al binario. El binario busca el dashboard en este orden:
|
||||
|
||||
1. Flag `--dashboard <path>` (solo funciona con `wails build`, no con `wails dev`)
|
||||
2. Variable de entorno `DASHBOARD` (no se propaga al binario en `wails dev`)
|
||||
3. Archivo `dashboard.yaml` en el cwd (metodo recomendado para desarrollo)
|
||||
|
||||
**Para desarrollo, usar un symlink:**
|
||||
|
||||
```bash
|
||||
# Apuntar al dashboard deseado
|
||||
ln -sf examples/fn_registry_overview.yaml dashboard.yaml
|
||||
|
||||
# Lanzar — el binario encuentra dashboard.yaml automaticamente
|
||||
wails dev
|
||||
```
|
||||
|
||||
### Produccion (wails build)
|
||||
|
||||
```bash
|
||||
# Compilar
|
||||
CGO_ENABLED=1 wails build -tags fts5
|
||||
|
||||
# Ejecutar con un YAML especifico
|
||||
./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml
|
||||
./build/bin/rapid-dashboards --dashboard /ruta/absoluta/mi_dashboard.yaml
|
||||
```
|
||||
|
||||
El binario compilado acepta el flag `--dashboard` directamente.
|
||||
|
||||
---
|
||||
|
||||
## Cambiar entre dashboards
|
||||
|
||||
### En vivo (dropdown en la app)
|
||||
|
||||
Si hay mas de un archivo `.yaml` en el directorio del dashboard actual, aparece un dropdown junto al titulo. Al seleccionar otro dashboard:
|
||||
|
||||
- Se cargan las nuevas conexiones, queries y widgets
|
||||
- Se cierran las conexiones anteriores
|
||||
- El tema se aplica automaticamente
|
||||
|
||||
Los dashboards disponibles se escanean del directorio donde esta el YAML actual (por defecto `examples/`).
|
||||
|
||||
### Cambiando el symlink (para desarrollo)
|
||||
|
||||
```bash
|
||||
# Ver dashboards disponibles
|
||||
ls examples/*.yaml
|
||||
|
||||
# Cambiar al dashboard de apps
|
||||
ln -sf examples/fn_registry_apps.yaml dashboard.yaml
|
||||
|
||||
# Cambiar al overview de funciones
|
||||
ln -sf examples/fn_registry_overview.yaml dashboard.yaml
|
||||
|
||||
# Reiniciar wails dev para que tome el cambio
|
||||
```
|
||||
|
||||
### Por CLI (produccion)
|
||||
|
||||
```bash
|
||||
# Dashboard de funciones
|
||||
./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml
|
||||
|
||||
# Dashboard de apps
|
||||
./build/bin/rapid-dashboards --dashboard examples/fn_registry_apps.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Temas
|
||||
|
||||
El campo `theme` del YAML controla toda la estetica del dashboard. Cada tema cambia colores de fondo, texto, cards, bordes, acentos y la paleta de colores de los graficos.
|
||||
|
||||
### Temas disponibles
|
||||
|
||||
| Tema | Descripcion | Uso recomendado |
|
||||
|------|-------------|-----------------|
|
||||
| `dark` | Azul-gris profundo, acentos azules | Default, dashboards tecnicos |
|
||||
| `emerald` | Verde esmeralda oscuro, acentos dorados | Dashboards financieros, naturaleza |
|
||||
| `amber` | Calido ambar/naranja sobre fondo oscuro | Dashboards de alertas, operaciones |
|
||||
| `rose` | Rosa/magenta sobre fondo oscuro | Dashboards de marketing, analytics |
|
||||
| `light` | Fondo claro con sombras sutiles | Presentaciones, pantallas con luz |
|
||||
|
||||
### Ejemplo de uso
|
||||
|
||||
```yaml
|
||||
# Dashboard con tema esmeralda
|
||||
theme: "emerald"
|
||||
|
||||
# Dashboard con tema claro
|
||||
theme: "light"
|
||||
```
|
||||
|
||||
### Que cambia cada tema
|
||||
|
||||
- **Background y foreground**: tonos base del fondo y texto
|
||||
- **Card**: color de fondo de widgets, sin bordes en temas oscuros, con sombra en light
|
||||
- **Primary/accent**: color de acento para interacciones y highlights
|
||||
- **Border**: tonos de separadores y bordes
|
||||
- **Chart palette**: cada tema tiene su propia secuencia de colores para graficos (`--chart-1` a `--chart-5`)
|
||||
|
||||
### Anadir un tema nuevo
|
||||
|
||||
Anadir un bloque `[data-theme="nombre"]` en `frontend/src/app.css` con todas las variables CSS. El tema se activa automaticamente al usar `theme: "nombre"` en el YAML.
|
||||
|
||||
---
|
||||
|
||||
## Estructura del YAML
|
||||
|
||||
Un dashboard se define con 5 bloques:
|
||||
|
||||
```yaml
|
||||
settings: # titulo, dimensiones, refresh global, columnas
|
||||
theme: # tema visual
|
||||
connections: # bases de datos
|
||||
queries: # SQL + refresh + params
|
||||
filters: # controles de filtro
|
||||
sections: # secciones con widgets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. settings
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
title: "Mi Dashboard" # titulo de la ventana
|
||||
refresh: 30s # refresh global por defecto
|
||||
width: 1280 # ancho ventana (px)
|
||||
height: 800 # alto ventana (px)
|
||||
columns: 12 # columnas del grid CSS
|
||||
```
|
||||
|
||||
- `refresh` acepta duraciones Go: `200ms`, `1s`, `5s`, `1m`, `30m`
|
||||
- `columns` define el grid base. Cada widget usa `span` para ocupar N columnas.
|
||||
|
||||
---
|
||||
|
||||
## 2. connections
|
||||
|
||||
Cada conexion tiene un nombre y un driver. Secrets via variables de entorno `${VAR}`.
|
||||
|
||||
### SQLite
|
||||
```yaml
|
||||
connections:
|
||||
local:
|
||||
driver: sqlite
|
||||
path: ./data.db
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
```yaml
|
||||
connections:
|
||||
main_db:
|
||||
driver: postgres
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: analytics
|
||||
password: "${PG_PASSWORD}"
|
||||
database: myapp
|
||||
sslmode: disable
|
||||
```
|
||||
|
||||
### DuckDB
|
||||
```yaml
|
||||
connections:
|
||||
warehouse:
|
||||
driver: duckdb
|
||||
path: ./warehouse.duckdb
|
||||
```
|
||||
|
||||
### ClickHouse
|
||||
```yaml
|
||||
connections:
|
||||
events:
|
||||
driver: clickhouse
|
||||
host: localhost
|
||||
port: 9000
|
||||
user: default
|
||||
password: ""
|
||||
database: events
|
||||
```
|
||||
|
||||
**Paths relativos**: se resuelven desde el cwd del binario, NO desde la ubicacion del YAML. En desarrollo el cwd es `apps/rapid_dashboards/`, asi que `../../registry.db` apunta a la raiz del repo.
|
||||
|
||||
---
|
||||
|
||||
## 3. queries
|
||||
|
||||
```yaml
|
||||
queries:
|
||||
revenue_total:
|
||||
connection: main_db
|
||||
sql: "SELECT SUM(amount) as value FROM orders"
|
||||
refresh: 10s
|
||||
stale_time: 5s
|
||||
|
||||
revenue_by_date:
|
||||
connection: main_db
|
||||
sql: |
|
||||
SELECT date, SUM(amount) as revenue
|
||||
FROM orders
|
||||
WHERE date >= :date_from AND date <= :date_to
|
||||
ORDER BY date
|
||||
params:
|
||||
date_from: "$filter.date_range.from"
|
||||
date_to: "$filter.date_range.to"
|
||||
```
|
||||
|
||||
### Parametros
|
||||
|
||||
- `:nombre` en el SQL se reemplaza por el valor del param.
|
||||
- `$filter.xxx` referencia el valor actual de un filtro.
|
||||
- `$filter.date_range.from` accede al subcampo `from`.
|
||||
- Placeholders se convierten al formato del driver (`$1` para Postgres, `?` para SQLite/DuckDB/ClickHouse).
|
||||
|
||||
### Fechas relativas
|
||||
|
||||
| Valor | Resultado |
|
||||
|-------|-----------|
|
||||
| `now` | timestamp actual |
|
||||
| `now-7d` | hace 7 dias |
|
||||
| `now-24h` | hace 24 horas |
|
||||
| `now-30m` | hace 30 minutos |
|
||||
|
||||
---
|
||||
|
||||
## 4. filters
|
||||
|
||||
### Select
|
||||
```yaml
|
||||
filters:
|
||||
category:
|
||||
type: select
|
||||
label: "Categoria"
|
||||
default: "all"
|
||||
options:
|
||||
- { label: "Todas", value: "all" }
|
||||
- { label: "Electronics", value: "electronics" }
|
||||
```
|
||||
|
||||
### Date Range
|
||||
```yaml
|
||||
filters:
|
||||
date_range:
|
||||
type: date_range
|
||||
label: "Periodo"
|
||||
default: { from: "now-7d", to: "now" }
|
||||
presets:
|
||||
- { label: "7 dias", from: "now-7d", to: "now" }
|
||||
- { label: "30 dias", from: "now-30d", to: "now" }
|
||||
```
|
||||
|
||||
### Text
|
||||
```yaml
|
||||
filters:
|
||||
search:
|
||||
type: text
|
||||
label: "Buscar"
|
||||
placeholder: "Buscar por nombre..."
|
||||
debounce: 300
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. sections y widgets
|
||||
|
||||
```yaml
|
||||
sections:
|
||||
- id: kpis
|
||||
title: "Metricas"
|
||||
columns: 4 # override de columnas
|
||||
widgets:
|
||||
- id: total_users
|
||||
type: kpi
|
||||
title: "Usuarios"
|
||||
query: count_users
|
||||
mapping: { value: "value" }
|
||||
span: 1
|
||||
|
||||
- id: charts
|
||||
title: "Graficos"
|
||||
collapsible: true
|
||||
columns: 2
|
||||
widgets:
|
||||
- id: revenue_line
|
||||
type: line_chart
|
||||
title: "Revenue"
|
||||
query: revenue_by_date
|
||||
mapping:
|
||||
x: "date"
|
||||
series:
|
||||
- { key: "revenue", name: "Revenue", color: "#3b82f6" }
|
||||
options:
|
||||
zoomable: true
|
||||
span: 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tipos de widget
|
||||
|
||||
### kpi
|
||||
|
||||
```yaml
|
||||
- type: kpi
|
||||
mapping:
|
||||
value: "campo_sql"
|
||||
format: "$,.2f" # opcional
|
||||
```
|
||||
|
||||
### line_chart
|
||||
|
||||
```yaml
|
||||
- type: line_chart
|
||||
mapping:
|
||||
x: "date"
|
||||
series:
|
||||
- { key: "revenue", name: "Revenue", color: "#3b82f6" }
|
||||
options:
|
||||
curve: monotone # linear | monotone | step
|
||||
show_grid: true
|
||||
show_legend: true
|
||||
zoomable: true
|
||||
height: 400
|
||||
```
|
||||
|
||||
### bar_chart
|
||||
|
||||
```yaml
|
||||
- type: bar_chart
|
||||
mapping:
|
||||
x: "category"
|
||||
y: "count"
|
||||
# O multi-series con series: [...]
|
||||
options:
|
||||
horizontal: true # barras horizontales
|
||||
show_grid: true
|
||||
show_legend: true
|
||||
height: 300
|
||||
```
|
||||
|
||||
### pie_chart
|
||||
|
||||
```yaml
|
||||
- type: pie_chart
|
||||
mapping:
|
||||
name: "domain" # campo para nombres de segmentos
|
||||
value: "cantidad" # campo numerico
|
||||
options:
|
||||
donut: true # hueco central
|
||||
show_legend: true # default: true
|
||||
height: 300
|
||||
```
|
||||
|
||||
### area_chart
|
||||
|
||||
```yaml
|
||||
- type: area_chart
|
||||
mapping:
|
||||
x: "date"
|
||||
series:
|
||||
- { key: "users", name: "Usuarios" }
|
||||
options:
|
||||
stacked: true
|
||||
height: 300
|
||||
```
|
||||
|
||||
### sparkline
|
||||
|
||||
```yaml
|
||||
- type: sparkline
|
||||
mapping:
|
||||
value: "metric"
|
||||
options:
|
||||
variant: area # line | area | bar
|
||||
width: 200
|
||||
height: 40
|
||||
```
|
||||
|
||||
### table
|
||||
|
||||
```yaml
|
||||
- type: table
|
||||
mapping:
|
||||
columns:
|
||||
- { key: "name", label: "Nombre" }
|
||||
- { key: "amount", label: "Monto", format: "$,.2f" }
|
||||
- { key: "created_at", label: "Fecha", format: "datetime" }
|
||||
options:
|
||||
heatmap_columns: ["go", "python", "bash"] # colorea celdas por intensidad
|
||||
```
|
||||
|
||||
Si no defines `columns`, se auto-detectan del resultado SQL.
|
||||
|
||||
**Heatmap**: `options.heatmap_columns` acepta un array de nombres de columna. Las celdas se colorean de azul oscuro (valor minimo) a azul brillante (valor maximo), calculado por columna.
|
||||
|
||||
---
|
||||
|
||||
## Formatos de valor
|
||||
|
||||
| Formato | Ejemplo | Resultado |
|
||||
|---------|---------|-----------|
|
||||
| `$,.2f` | 1234.5 | $1,234.50 |
|
||||
| `,.2f` | 1234.5 | 1,234.50 |
|
||||
| `.2f` | 1234.5 | 1234.50 |
|
||||
| `,` | 1234 | 1,234 |
|
||||
| `datetime` | ISO string | locale datetime |
|
||||
|
||||
---
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
Cualquier campo del YAML soporta `${VARIABLE}`:
|
||||
|
||||
```bash
|
||||
DB_HOST=prod.example.com DB_PASSWORD=secret \
|
||||
./rapid-dashboards --dashboard prod.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Puntos clave (aprendizajes)
|
||||
|
||||
### Wails dev no pasa CLI args al binario
|
||||
|
||||
`wails dev -- --flag value` no funciona: Wails compila y ejecuta el binario como proceso hijo sin pasar los args despues de `--`. Tampoco propaga variables de entorno al binario. La solucion es usar un archivo `dashboard.yaml` en el cwd que el binario detecta automaticamente.
|
||||
|
||||
### Wails genera bindings ejecutando el binario sin args
|
||||
|
||||
Durante `wails dev`, el primer paso es generar bindings TypeScript. Para esto Wails ejecuta el binario sin argumentos. Si el binario requiere flags obligatorios y hace `os.Exit(1)`, la generacion falla. Solucion: detectar cuando no hay args y usar un config dummy con slices vacios (no nil) para que los tipos se generen correctamente.
|
||||
|
||||
### Slices nil de Go se serializan como null en JSON
|
||||
|
||||
`[]SectionDef(nil)` se serializa como `null`, no como `[]`. Si el frontend hace `.map()` sobre null, crash. Inicializar siempre con `[]SectionDef{}` para el config dummy, y usar `(config.sections ?? [])` en el frontend.
|
||||
|
||||
### Tipos de Wails vs tipos manuales de TypeScript
|
||||
|
||||
Wails genera `models.ts` con tipos `string` genericos. Si el frontend tiene tipos mas estrictos (union types como `"select" | "date_range"`), hay incompatibilidad. Solucion: castear los imports con `as unknown as typeof X`.
|
||||
|
||||
### Paths relativos se resuelven desde el cwd, no desde el YAML
|
||||
|
||||
`SQLiteOpen(path)` resuelve paths relativos al cwd del proceso. Si el YAML esta en `examples/` y el binario corre desde `apps/rapid_dashboards/`, un path `../../registry.db` es correcto pero `../../../registry.db` (relativo al YAML) no. Siempre pensar los paths desde el cwd del binario.
|
||||
|
||||
### Recharts Pie necesita valores numericos
|
||||
|
||||
SQLite puede devolver numeros como strings en algunos contextos. Recharts Pie no renderiza nada si `dataKey` apunta a un string. Solucion: coercer a `Number()` antes de pasar a Pie.
|
||||
|
||||
### Recharts PieLabelRenderProps es estricto
|
||||
|
||||
El tipo `label` de `<Pie>` no acepta callbacks con tipos genericos como `Record<string, unknown>`. Hay que importar `PieLabelRenderProps` de `recharts` y usar una funcion con nombre tipada, no un lambda inline.
|
||||
|
||||
### Animaciones de Recharts
|
||||
|
||||
Las animaciones por defecto de Recharts Pie son lentas en dashboards que refrescan frecuentemente. Usar `isAnimationActive={false}` para render instantaneo.
|
||||
|
||||
---
|
||||
|
||||
## Scripts de lanzamiento
|
||||
|
||||
Cada dashboard YAML debe tener forma de lanzarse rapido desde `scripts/`. La carpeta `scripts/` contiene dos scripts genericos que reciben el nombre del dashboard como argumento:
|
||||
|
||||
```bash
|
||||
# Listar dashboards disponibles
|
||||
./scripts/dev.sh
|
||||
|
||||
# Desarrollo — crea symlink + wails dev
|
||||
./scripts/dev.sh fn_registry_overview
|
||||
|
||||
# Produccion — compila si es necesario + ejecuta
|
||||
./scripts/prod.sh fn_registry_apps
|
||||
|
||||
# Produccion con variables de entorno
|
||||
DB_PASSWORD=secret ./scripts/prod.sh mi_dashboard
|
||||
```
|
||||
|
||||
### Convencion
|
||||
|
||||
Al crear un nuevo YAML en `examples/`, no hace falta crear scripts adicionales — `dev.sh` y `prod.sh` descubren automaticamente los `.yaml` de `examples/`. Solo ejecutar con el nombre (sin extension):
|
||||
|
||||
```bash
|
||||
# Nuevo dashboard
|
||||
vim examples/mi_nuevo_dashboard.yaml
|
||||
|
||||
# Lanzar inmediatamente
|
||||
./scripts/dev.sh mi_nuevo_dashboard
|
||||
```
|
||||
|
||||
`dev.sh` crea el symlink `dashboard.yaml` y lanza `wails dev`. `prod.sh` compila con `CGO_ENABLED=1 wails build -tags fts5` si el binario no existe o el YAML es mas reciente, y ejecuta con `--dashboard`.
|
||||
|
||||
---
|
||||
|
||||
## Compilacion multiplataforma
|
||||
|
||||
### Linux (default)
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 wails build -tags fts5
|
||||
```
|
||||
|
||||
Genera `build/bin/rapid-dashboards` con soporte para SQLite, PostgreSQL, DuckDB y ClickHouse.
|
||||
|
||||
### Windows (cross-compile desde Linux)
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 \
|
||||
wails build -tags "fts5 noduckdb noclickhouse" -platform windows/amd64 -skipbindings
|
||||
```
|
||||
|
||||
Genera `build/bin/rapid-dashboards.exe`. Requiere `mingw-w64` instalado (`apt install gcc-mingw-w64-x86-64`).
|
||||
|
||||
**Limitacion:** DuckDB y ClickHouse se excluyen con los tags `noduckdb` y `noclickhouse` porque `go-duckdb` depende de libstdc++ y no cross-compila bien desde Linux. El `.exe` solo soporta SQLite y PostgreSQL. Si un YAML usa esos drivers, dara error: `"duckdb support not compiled"`.
|
||||
|
||||
### Windows nativo (con DuckDB y ClickHouse)
|
||||
|
||||
Para compilar con todos los drivers, compilar directamente en Windows con Go y MinGW/MSYS2 instalados:
|
||||
|
||||
```powershell
|
||||
# En Windows con Go + MinGW (MSYS2)
|
||||
$env:CGO_ENABLED=1
|
||||
wails build -tags fts5
|
||||
```
|
||||
|
||||
Esto genera un `.exe` con soporte completo: SQLite, PostgreSQL, DuckDB y ClickHouse.
|
||||
|
||||
### Resumen de soporte por plataforma
|
||||
|
||||
| Driver | Linux | Windows (cross-compile) | Windows (nativo) |
|
||||
|---|---|---|---|
|
||||
| SQLite | si | si | si |
|
||||
| PostgreSQL | si | si | si |
|
||||
| DuckDB | si | no | si |
|
||||
| ClickHouse | si | no | si |
|
||||
|
||||
### Rutas en Windows
|
||||
|
||||
Las rutas no se rompen entre plataformas. El codigo usa `filepath.Join` y `filepath.IsAbs` de Go, que adaptan los separadores automaticamente (`/` en Linux, `\` en Windows). Los paths relativos en el YAML (ej: `../../registry.db`) funcionan igual en ambos OS.
|
||||
|
||||
---
|
||||
|
||||
## Extensibilidad
|
||||
|
||||
Para anadir un tipo de widget nuevo:
|
||||
|
||||
1. Crear `frontend/src/components/widgets/MiWidget.tsx` implementando `WidgetProps`
|
||||
2. Registrar en `frontend/src/components/widgets/register.ts`: `registerWidget('mi_tipo', MiWidget)`
|
||||
3. Usar en YAML: `type: mi_tipo`
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
# Schema YAML — Referencia completa
|
||||
|
||||
Referencia rapida de todos los campos del YAML de dashboard.
|
||||
|
||||
## Top level
|
||||
|
||||
| Campo | Tipo | Requerido | Default | Descripcion |
|
||||
|-------|------|-----------|---------|-------------|
|
||||
| `settings` | object | si | — | Configuracion global |
|
||||
| `theme` | string | no | `"dark"` | Nombre del tema |
|
||||
| `connections` | map | si | — | Conexiones a bases de datos |
|
||||
| `queries` | map | si | — | Definiciones de queries SQL |
|
||||
| `filters` | map | no | `{}` | Filtros interactivos |
|
||||
| `sections` | array | si | — | Secciones con widgets |
|
||||
|
||||
## settings
|
||||
|
||||
| Campo | Tipo | Default | Descripcion |
|
||||
|-------|------|---------|-------------|
|
||||
| `title` | string | — | Titulo de la ventana (requerido) |
|
||||
| `refresh` | duration | `"30s"` | Refresh global por defecto |
|
||||
| `width` | int | `1280` | Ancho de ventana en px |
|
||||
| `height` | int | `800` | Alto de ventana en px |
|
||||
| `columns` | int | `12` | Columnas del grid CSS |
|
||||
|
||||
## connections[nombre]
|
||||
|
||||
| Campo | Tipo | Drivers | Descripcion |
|
||||
|-------|------|---------|-------------|
|
||||
| `driver` | string | todos | `sqlite` \| `postgres` \| `duckdb` \| `clickhouse` |
|
||||
| `path` | string | sqlite, duckdb | Ruta al archivo de BD |
|
||||
| `host` | string | postgres, clickhouse | Hostname |
|
||||
| `port` | int | postgres, clickhouse | Puerto (default: 5432/9000) |
|
||||
| `user` | string | postgres, clickhouse | Usuario |
|
||||
| `password` | string | postgres, clickhouse | Password (soporta `${ENV}`) |
|
||||
| `database` | string | postgres, clickhouse | Nombre de la base de datos |
|
||||
| `sslmode` | string | postgres | Modo SSL (default: `"disable"`) |
|
||||
|
||||
## queries[nombre]
|
||||
|
||||
| Campo | Tipo | Default | Descripcion |
|
||||
|-------|------|---------|-------------|
|
||||
| `connection` | string | — | Nombre de la conexion (requerido) |
|
||||
| `sql` | string | — | Query SQL (requerido) |
|
||||
| `refresh` | duration | global | Intervalo de re-ejecucion |
|
||||
| `stale_time` | duration | refresh/2 | Tiempo antes de considerar dato viejo |
|
||||
| `params` | map | `{}` | Parametros: `nombre: valor_o_$filter.ref` |
|
||||
|
||||
## filters[nombre]
|
||||
|
||||
| Campo | Tipo | Tipos de filtro | Descripcion |
|
||||
|-------|------|-----------------|-------------|
|
||||
| `type` | string | todos | `select` \| `date_range` \| `text` |
|
||||
| `label` | string | todos | Etiqueta visible |
|
||||
| `default` | any | todos | Valor por defecto |
|
||||
| `options` | array | select | `[{ label, value }]` |
|
||||
| `presets` | array | date_range | `[{ label, from, to }]` |
|
||||
| `placeholder` | string | text | Placeholder del input |
|
||||
| `debounce` | int | text | Delay en ms (default: 300) |
|
||||
|
||||
## sections[]
|
||||
|
||||
| Campo | Tipo | Default | Descripcion |
|
||||
|-------|------|---------|-------------|
|
||||
| `id` | string | — | Identificador unico (requerido) |
|
||||
| `title` | string | — | Titulo visible |
|
||||
| `collapsible` | bool | `false` | Permite colapsar |
|
||||
| `columns` | int | global | Override de columnas del grid |
|
||||
| `widgets` | array | — | Widgets de esta seccion |
|
||||
|
||||
## widgets[]
|
||||
|
||||
| Campo | Tipo | Default | Descripcion |
|
||||
|-------|------|---------|-------------|
|
||||
| `id` | string | — | Identificador unico (requerido) |
|
||||
| `type` | string | — | Tipo de componente (requerido) |
|
||||
| `title` | string | — | Titulo del widget |
|
||||
| `query` | string | — | Nombre de la query (requerido) |
|
||||
| `mapping` | object | — | Mapeo de campos SQL a props |
|
||||
| `options` | object | `{}` | Opciones del componente |
|
||||
| `span` | int | `1` | Columnas que ocupa |
|
||||
| `row_span` | int | `1` | Filas que ocupa |
|
||||
|
||||
## Tipos de widget y sus mappings
|
||||
|
||||
### kpi
|
||||
```
|
||||
mapping.value → campo del resultado (string)
|
||||
mapping.format → formato de visualizacion (string, opcional)
|
||||
```
|
||||
|
||||
### line_chart / bar_chart / area_chart
|
||||
```
|
||||
mapping.x → campo del eje X (string)
|
||||
mapping.y → campo del eje Y — single series (string)
|
||||
mapping.series → multi-series: [{ key, name, color? }]
|
||||
```
|
||||
|
||||
### sparkline
|
||||
```
|
||||
mapping.value → campo numerico (string)
|
||||
```
|
||||
|
||||
### table
|
||||
```
|
||||
mapping.columns → [{ key, label, format? }]
|
||||
Si no se define, auto-detecta del resultado.
|
||||
```
|
||||
|
||||
## Options por tipo
|
||||
|
||||
### line_chart
|
||||
| Option | Tipo | Default | Descripcion |
|
||||
|--------|------|---------|-------------|
|
||||
| `curve` | string | `"monotone"` | linear, monotone, step, stepBefore, stepAfter |
|
||||
| `show_grid` | bool | `true` | Mostrar cuadricula |
|
||||
| `show_legend` | bool | `false` | Mostrar leyenda |
|
||||
| `zoomable` | bool | `false` | Habilitar brush zoom |
|
||||
| `height` | int | `300` | Altura en px |
|
||||
|
||||
### bar_chart
|
||||
| Option | Tipo | Default | Descripcion |
|
||||
|--------|------|---------|-------------|
|
||||
| `horizontal` | bool | `false` | Barras horizontales |
|
||||
| `show_grid` | bool | `true` | Mostrar cuadricula |
|
||||
| `show_legend` | bool | `false` | Mostrar leyenda |
|
||||
| `height` | int | `300` | Altura en px |
|
||||
|
||||
### area_chart
|
||||
| Option | Tipo | Default | Descripcion |
|
||||
|--------|------|---------|-------------|
|
||||
| `stacked` | bool | `false` | Apilar areas |
|
||||
| `show_grid` | bool | `true` | Mostrar cuadricula |
|
||||
| `show_legend` | bool | `false` | Mostrar leyenda |
|
||||
| `height` | int | `300` | Altura en px |
|
||||
|
||||
### sparkline
|
||||
| Option | Tipo | Default | Descripcion |
|
||||
|--------|------|---------|-------------|
|
||||
| `variant` | string | `"area"` | line, area, bar |
|
||||
| `width` | int | `200` | Ancho en px |
|
||||
| `height` | int | `40` | Altura en px |
|
||||
|
||||
## Duraciones validas
|
||||
|
||||
| Formato | Ejemplo |
|
||||
|---------|---------|
|
||||
| milisegundos | `100ms`, `200ms`, `500ms` |
|
||||
| segundos | `1s`, `5s`, `30s` |
|
||||
| minutos | `1m`, `5m`, `30m` |
|
||||
| horas | `1h` |
|
||||
| combinado | `1m30s` |
|
||||
|
||||
## Fechas relativas
|
||||
|
||||
| Formato | Resultado |
|
||||
|---------|-----------|
|
||||
| `now` | momento actual |
|
||||
| `now-Ns` | hace N segundos |
|
||||
| `now-Nm` | hace N minutos |
|
||||
| `now-Nh` | hace N horas |
|
||||
| `now-Nd` | hace N dias |
|
||||
|
||||
## KPI mappings extendidos (v2)
|
||||
|
||||
Ademas de `value` y `format`, el widget KPI soporta:
|
||||
|
||||
| Mapping | Tipo | Descripcion |
|
||||
|---------|------|-------------|
|
||||
| `value` | string | Campo SQL del valor principal |
|
||||
| `format` | string | Formato: `$,.2f`, `,`, `datetime` |
|
||||
| `unit` | string | Campo SQL para la unidad (ej: "k", "ms") |
|
||||
| `unitLabel` | string | Unidad fija (alternativa a `unit` dinamico) |
|
||||
| `delta` | string | Campo SQL para el cambio porcentual |
|
||||
| `deltaLabel` | string | Texto antes del delta: "Increased by" |
|
||||
| `deltaSuffix` | string | Texto despues del delta: "vs yesterday" |
|
||||
| `sparkline` | string | Campo SQL con valores para mini barras |
|
||||
| `sparklineColors` | string[] | Colores por barra |
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```yaml
|
||||
- type: kpi
|
||||
mapping:
|
||||
value: "total"
|
||||
format: ","
|
||||
unitLabel: "k"
|
||||
delta: "pct_change"
|
||||
deltaLabel: "Increased by"
|
||||
deltaSuffix: "vs yesterday"
|
||||
sparkline: "daily_value"
|
||||
sparklineColors: ["#3b82f6", "#8b5cf6", "#f59e0b", "#10b981"]
|
||||
```
|
||||
|
||||
## Scripts de lanzamiento
|
||||
|
||||
```bash
|
||||
./scripts/dev.sh <nombre> # symlink + wails dev
|
||||
./scripts/prod.sh <nombre> # build + ejecutar
|
||||
```
|
||||
|
||||
Sin argumento lista los dashboards disponibles en `examples/`.
|
||||
Reference in New Issue
Block a user