🧩Widgets del Panel
El Panel es una cuadrícula de tarjetas. Arrastra para reordenar, oculta las que no necesites (menú de diseño), y actualiza una tarjeta con su botón de actualizar.
Las tarjetas se construyen solas al aparecer: los números cuentan hasta su valor, las listas y tablas suben fila por fila, las barras de los gráficos crecen y las líneas se dibujan, las barras de progreso se llenan, y Agenda de hoy abre la línea de cada tarea y deja sus ejecuciones sobre ella de izquierda a derecha. Una tarjeta más abajo en la página espera a que llegues hasta ella, así que tú también la ves. Cuando el panel de notificaciones se abre o se cierra, las tarjetas se deslizan a su nuevo sitio. Todo se queda quieto cuando tu sistema pide reducir el movimiento.
Una instalación nueva muestra Actividad, Servidor, Agenda de hoy, Tareas y Hazlo tuyo. Las demás están ocultas hasta que las necesites (Editar diseño las muestra); Shopify aparece solo en cuanto se conecta una tienda.
Hazlo tuyo es la tarjeta del final: haz clic en un ejemplo ("Ventas de hoy en Shopify", "Visitantes de mi
sitio web"...) y se abre una sesión nueva en system/harness con la petición ya escrita: envíala, y
Harness escribe el widget en tus extensiones. Cualquier petición funciona igual desde cualquier sesión: "añade un widget
que muestre ...". Oculta la tarjeta con Editar diseño cuando ya tengas el tuyo.
Widgets integrados
| Widget | Qué muestra |
|---|---|
| Actividad | sesiones recientes, turnos en curso, respuestas sin leer (antes de la primera sesión: por dónde empezar) |
| Tareas | en curso, para hoy, vencidas |
| Agenda de hoy | el día de tus tareas programadas en una línea de 00:00 - 24:00: una fila por tarea, ejecuciones hechas (en verde, en rojo si fallaron), ejecuciones por venir (huecas), la que se está ejecutando ahora (parpadeando) y una línea de "ahora"; pasa el cursor sobre un punto para ver su hora y duración |
| Tareas programadas | próximas ejecuciones y fallos recientes |
| Servidor | CPU, memoria, disco, tiempo en marcha, problemas de salud |
| Copia de seguridad de la memoria | última ejecución, antigüedad, repositorio |
| Telegram | estado del bot y del enlace |
| Shopify | ingresos de hoy y de los últimos 7 días (frente a la semana anterior), pedidos de hoy, pedidos por enviar, gráfico de ingresos de 14 días, últimos pedidos — una vez conectada una tienda (más abajo) |
| Hazlo tuyo | cómo conseguir tus propios widgets, con ejemplos que abren una sesión lista para enviar |
| Uso de Claude | desactivado por defecto (Ajustes -> Funciones): ventanas de uso del plan, igual que las barras de la parte inferior de la barra lateral (se leen de la última respuesta de Claude; si no llegó ninguna en 10 minutos, consulta un endpoint no documentado de Anthropic, que podría dejar de funcionar; cuando ese endpoint falla o pide esperar, se siguen mostrando las últimas cifras leídas de Claude). |
Conectar una tienda Shopify
El widget de Shopify muestra una tarjeta "Conecta tu tienda" hasta que añades dos claves:
- En el panel de administración de Shopify: Settings → Apps and sales channels → Develop apps → Create an app, luego
Configure Admin API scopes → marca
read_orders→ Install app → muestra el Admin API access token (shpat_…, se muestra una sola vez). - En Harness Maxxing: Ajustes → Claves → añade
SHOPIFY_STORE=myshop.myshopify.comySHOPIFY_ADMIN_TOKEN= el token.
El widget se actualiza cada 2 minutos (o con su botón de actualizar) y solo lee pedidos. ¿Otra tienda (WooCommerce, Stripe, PrestaShop…)? Pídele a Claude un widget: construye una extensión de la misma manera (ver más abajo).
Escribir tu propio widget
Un widget es una carpeta en <workspace>/extensions/<id>/ con dos archivos. Escribes datos, no
HTML: la tarjeta la dibuja el sistema de diseño de la app, así que siempre se ve bien en modo oscuro y claro,
en ordenador y en móvil. (¿Necesitas dibujo libre o interacción, como un juego o un canvas? Mira
widgets HTML más abajo.)
La forma más rápida: pídele a Claude en una sesión, "crea un widget de panel que muestre ...". La
skill maxxing-extension conoce todo lo que sigue.
1. Crea la carpeta
/root/maxxing/extensions/disk-usage/
manifest.json
api.py
El id (nombre de la carpeta) es un slug en minúsculas: letras, dígitos y guiones. Ids reservados: activity,
tasks, day, crons, server, backup, telegram, shopify, usage, customize.
2. manifest.json
{"id": "disk-usage", "name": "Disk usage", "type": "widget", "icon": "hard-drive",
"version": "1.0.0", "api": 1, "span": 1, "ttl": 60}
| Clave | Significado |
|---|---|
type |
widget |
api |
versión del contrato de extensiones para la que está escrito (por defecto 1); si una versión deja de ser compatible, la comprobación previa a la actualización te avisa antes de actualizar |
icon |
un nombre de icono en kebab-case (gauge, chart-line, shopping-cart, users, receipt...) del conjunto de iconos lucide de la app; un nombre que la app no incluye muestra una pieza de puzle |
span |
ancho en columnas de la cuadrícula: 1, 2 o 3 |
ttl |
segundos que se guarda en caché el resultado (por defecto 300). El botón de actualizar lo ignora. |
3. api.py
import shutil
def card(ctx):
du = shutil.disk_usage("/")
pct = round(100 * (du.total - du.free) / du.total)
return {
"title": "Disk usage",
"status": "error" if pct > 95 else "warn" if pct > 85 else "ok",
"blocks": [
{"type": "stat", "label": "Used", "value": f"{pct}%", "hint": f"{du.free / 1e9:.1f} GB free"},
{"type": "progress", "label": "/", "value": pct, "max": 100},
],
}
card(ctx) se ejecuta dentro del servidor: usa solo la biblioteca estándar de Python y mantén las llamadas de red cortas
(timeout ≤ 10 s). El código de las extensiones se ejecuta en 4 hilos propios: un código que nunca termina (una llamada de red sin
timeout) solo bloquea a otras extensiones ("las extensiones están ocupadas" hasta que termine), nunca al resto de Harness. Un
api.py debe cargarse en menos de 10 s (sin llamadas de red al importar), o la extensión muestra un error. ctx contiene:
| Clave | Valor |
|---|---|
id |
el id de la extensión |
workspace |
ruta del workspace |
state_dir |
una carpeta reservada para esta extensión (caché, datos) |
secret(name) |
valor de una clave de Ajustes -> Claves, o None |
config |
{"timezone", "language", "public_url"} |
Nunca escribas un token directamente en el código: guárdalo en Ajustes -> Claves y léelo con ctx["secret"]("MY_TOKEN").
4. El esquema de la tarjeta
Card = {title?, status?: "ok"|"warn"|"error", link?, blocks: [Block], updated?}
Block =
{type: "stat", label, value, hint?, delta?, tone?: "ok"|"warn"|"error"}
| {type: "stats", items: [stat, ...]} several stats in a row
| {type: "list", items: [{title, subtitle?, value?, href?, status?, icon?, emoji?}], empty?, grid?}
grid: true = tiles side by side (shortcuts)
icon: an icon name (below), drawn like the app's
| {type: "chart", kind: "bar"|"line", series: [{name, points: [[x, y], ...]}], unit?}
| {type: "table", columns: [str], rows: [[cell, ...]]}
| {type: "progress", label, value, max, hint?}
| {type: "markdown", text}
link hace que el título de la tarjeta sea clicable (una ruta de la app como /tasks o una URL https://). Los tipos
de bloque desconocidos se ignoran.
Ejemplo de lista + gráfico:
def card(ctx):
orders = [("#1042", "Alice", 89.0), ("#1041", "Bob", 34.5)]
return {
"title": "Shop",
"link": "https://shop.example.com/admin",
"blocks": [
{"type": "stats", "items": [{"type": "stat", "label": "Today", "value": "123.50 €"},
{"type": "stat", "label": "Orders", "value": "2"}]},
{"type": "chart", "kind": "bar", "unit": "€",
"series": [{"name": "Sales", "points": [["Mon", 80], ["Tue", 120], ["Wed", 123.5]]}]},
{"type": "list", "empty": "No orders yet",
"items": [{"title": o, "subtitle": c, "value": f"{v:.2f} €"} for o, c, v in orders]},
],
}
5. Prueba y carga
Prueba la función directamente en el servidor:
cd /root/maxxing/extensions/disk-usage && python3 -c "
import json, api, tempfile
ctx = {'id': 'disk-usage', 'workspace': '/root/maxxing', 'state_dir': tempfile.mkdtemp(),
'secret': lambda n: None, 'config': {'timezone': 'UTC', 'language': 'en', 'public_url': ''}}
print(json.dumps(api.card(ctx), indent=2))"
Después ve a Ajustes -> Extensiones -> Recargar. El widget aparece en el panel. Si algo
va mal (manifiesto no válido, error de importación, excepción en card), el error se muestra en la fila de la extensión
y en la tarjeta; el resto del panel sigue funcionando.
Las extensiones viven en tu workspace: las actualizaciones nunca las tocan y la copia de seguridad las incluye.
Widgets HTML (juegos, dibujos personalizados)
Cuando una tarjeta no basta (un pequeño juego, un dibujo en canvas, una biblioteca de gráficos propia), escribe el
widget en HTML. Misma carpeta, con widget.html en lugar de api.py (o junto a él):
/root/maxxing/extensions/snake-widget/
manifest.json {"type": "widget", "name": "Snake", "icon": "gamepad-2", "height": 300}
widget.html one self-contained file: inline <style> and <script>
api.py optional: handle(ctx, method, path, query, body) for data it saves or fetches
El panel dibuja widget.html en un frame aislado (sandbox): los scripts se ejecutan, pero el frame no puede leer
la app, sus cookies ni su API: un widget nunca puede actuar en tu nombre. Habla con su propio api.py
a través de un pequeño puente que se inyecta por ti:
| En widget.html | Qué hace |
|---|---|
await maxxing.api("best") · maxxing.api("best", {method: "PUT", body: {...}}) |
llama a handle() en el api.py de esta extensión (/api/ext/<id>/best) y devuelve su JSON |
maxxing.resize(400) |
fija la altura del frame en px (el height del manifiesto, 120-800, es el valor por defecto) |
maxxing.theme · maxxing.onTheme(fn) |
"dark" o "light", y una función de retorno cuando cambia |
class="ui-card", ui-btn, ui-input, ui-badge, ui-stat... |
el kit de UI de la app (ver Extensiones de página), ya cargado en el frame |
var(--card) --foreground --muted-foreground --border --primary... (y los antiguos --mx-fg --mx-muted --mx-border --mx-accent --mx-card --mx-bg) |
los colores de la app, actualizados con el tema y el color de acento |
Reglas: un solo archivo (ningún otro archivo de la carpeta se sirve al frame: incluye todo en línea, o
carga bibliotecas desde un CDN <script src="https://...">); sin localStorage ni cookies (el sandbox no
tiene: guarda a través de api.py y ctx["state_dir"]). El botón de actualizar de la tarjeta recarga el
frame. Ejemplo completo: examples/extensions/snake-widget/ en el directorio del código.