🧩Widgets do Painel
O Painel é uma grade de cartões. Arraste para reordenar, oculte os que você não precisa (menu de layout) e atualize um cartão com o botão de atualizar dele.
Os cartões se montam sozinhos conforme aparecem: os números sobem até o valor final, listas e tabelas surgem linha por linha, as barras dos gráficos crescem e as linhas se desenham, as barras de progresso se preenchem, e a Agenda de hoje abre a linha de cada tarefa e coloca as execuções nela, da esquerda para a direita. Um cartão mais abaixo na página espera você rolar até ele, então você também o vê se montando. Quando o painel de notificações abre ou fecha, os cartões deslizam para o novo lugar. Tudo fica parado quando seu sistema pede movimento reduzido.
Uma instalação nova mostra Atividade, Servidor, Agenda de hoje, Tarefas e Faça do seu jeito. Os outros ficam ocultos até você precisar deles (Editar layout mostra todos); o Shopify aparece sozinho assim que uma loja é conectada.
Faça do seu jeito é o cartão do final: clique em um exemplo ("Vendas de hoje no Shopify", "Visitantes do meu
site"...) e uma nova sessão abre em system/harness com o pedido já digitado - envie, e
o Harness escreve o widget nas suas extensões. Qualquer pedido funciona do mesmo jeito em qualquer sessão: "adicione um widget
que mostre ...". Oculte o cartão com Editar layout quando já tiver o seu.
Widgets integrados
| Widget | O que mostra |
|---|---|
| Atividade | sessões recentes, turnos em andamento, respostas não lidas (antes da primeira sessão: por onde começar) |
| Tarefas | em andamento, para hoje, atrasadas |
| Agenda de hoje | o dia das suas tarefas agendadas em uma linha de 00:00 a 24:00: uma linha por tarefa, execuções concluídas (verde, vermelho se falhou), execuções que ainda vão acontecer (vazadas), a que está rodando agora (pulsando) e uma linha de "agora"; passe o mouse sobre um ponto para ver o horário e a duração |
| Tarefas agendadas | próximas execuções e falhas recentes |
| Servidor | CPU, memória, disco, tempo ligado, problemas de saúde |
| Backup da memória | última execução, idade, repositório |
| Telegram | status do bot e da conexão |
| Shopify | faturamento de hoje e dos últimos 7 dias (em comparação com a semana anterior), pedidos de hoje, pedidos a enviar, gráfico de faturamento dos últimos 14 dias, últimos pedidos — depois que uma loja é conectada (veja abaixo) |
| Faça do seu jeito | como ter seus próprios widgets, com exemplos que abrem uma sessão pronta para enviar |
| Uso do Claude | desativado por padrão (Configurações -> Recursos): janelas de uso do plano, as mesmas das barras na parte de baixo da barra lateral (lido da última resposta do Claude; quando nenhuma chega em 10 minutos, consulta um endpoint não documentado da Anthropic, que pode parar de funcionar; quando esse endpoint falha ou pede para esperar, os últimos números lidos do Claude continuam sendo exibidos). |
Conectar uma loja Shopify
O widget do Shopify mostra um cartão "Conecte sua loja" até você adicionar duas chaves:
- No admin do Shopify: Configurações → Apps e canais de vendas → Desenvolver apps → Criar um app, depois
Configurar escopos da Admin API → marque
read_orders→ Instalar app → revele o token de acesso da Admin API (shpat_…, exibido uma única vez). - No Harness Maxxing: Configurações → Chaves → adicione
SHOPIFY_STORE=myshop.myshopify.comeSHOPIFY_ADMIN_TOKEN= o token.
O widget é atualizado a cada 2 minutos (ou pelo botão de atualizar) e apenas lê pedidos. Outra loja (WooCommerce, Stripe, PrestaShop…)? Peça um widget ao Claude: ele cria uma extensão do mesmo jeito (veja abaixo).
Escrevendo seu próprio widget
Um widget é uma pasta em <workspace>/extensions/<id>/ com dois arquivos. Você escreve dados, não
HTML: o cartão é desenhado pelo design system do app, então ele sempre fica bonito nos modos escuro e claro,
no computador e no celular. (Precisa de desenho livre ou interação - um jogo, um canvas? Veja
Widgets HTML abaixo.)
O jeito mais rápido: peça ao Claude em uma sessão, "crie um widget de painel que mostre ...". A skill
maxxing-extension conhece tudo o que está abaixo.
1. Crie a pasta
/root/maxxing/extensions/disk-usage/
manifest.json
api.py
O id (nome da pasta) é um slug em minúsculas: letras, dígitos, hifens. 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}
| Chave | Significado |
|---|---|
type |
widget |
api |
versão do contrato de extensões para a qual ele foi escrito (padrão 1); uma versão que deixe de oferecer suporte a ela é avisada pela verificação prévia da atualização antes de você atualizar |
icon |
um nome de ícone em kebab-case (gauge, chart-line, shopping-cart, users, receipt...) do conjunto de ícones lucide do app; um nome que o app não inclui mostra uma peça de quebra-cabeça |
span |
largura em colunas da grade: 1, 2 ou 3 |
ttl |
segundos em que o resultado fica em cache (padrão 300). O botão de atualizar ignora o cache. |
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) roda dentro do servidor: use apenas a biblioteca padrão do Python e mantenha as chamadas de rede curtas
(timeout ≤ 10 s). O código das extensões roda em 4 threads próprias: um código que nunca retorna (uma chamada de rede sem
timeout) só bloqueia as outras extensões ("as extensões estão ocupadas" até ele retornar), nunca o resto do Harness. Um
api.py precisa carregar em menos de 10 s (sem chamada de rede na importação), senão a extensão mostra um erro. O ctx contém:
| Chave | Valor |
|---|---|
id |
o id da extensão |
workspace |
caminho do workspace |
state_dir |
uma pasta reservada para esta extensão (cache, dados) |
secret(name) |
valor de uma chave de Configurações -> Chaves, ou None |
config |
{"timezone", "language", "public_url"} |
Nunca escreva um token direto no código: guarde-o em Configurações -> Chaves e leia com ctx["secret"]("MY_TOKEN").
4. O esquema do Card
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 torna o título do cartão clicável (uma rota do app como /tasks ou uma URL https://). Tipos de
bloco desconhecidos são ignorados.
Exemplo 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. Teste e depois carregue
Teste a função diretamente no 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))"
Depois vá em Configurações -> Extensões -> Recarregar. O widget aparece no painel. Se algo estiver
errado (manifesto inválido, erro de importação, exceção em card), o erro é mostrado na linha da extensão
e no cartão; o resto do painel continua funcionando.
As extensões ficam no seu workspace: as atualizações nunca mexem nelas e o backup as inclui.
Widgets HTML (jogos, desenhos personalizados)
Quando um Card não basta - um joguinho, um desenho em canvas, uma biblioteca de gráficos própria - escreva o
widget em HTML. Mesma pasta, com widget.html no lugar de api.py (ou junto com ele):
/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
O painel desenha o widget.html em um frame isolado (sandbox): os scripts rodam, mas o frame não pode ler
o app, seus cookies nem sua API - um widget nunca pode agir como você. Ele conversa com o próprio api.py
por meio de uma pequena ponte que é injetada para você:
| No widget.html | O que faz |
|---|---|
await maxxing.api("best") · maxxing.api("best", {method: "PUT", body: {...}}) |
chama handle() no api.py desta extensão (/api/ext/<id>/best) e retorna o JSON dele |
maxxing.resize(400) |
define a altura do frame em px (o height do manifesto, de 120 a 800, é o padrão) |
maxxing.theme · maxxing.onTheme(fn) |
"dark" ou "light", e um callback quando muda |
class="ui-card", ui-btn, ui-input, ui-badge, ui-stat... |
o kit de UI do app (veja Extensões de página), já carregado no frame |
var(--card) --foreground --muted-foreground --border --primary... (e os mais antigos --mx-fg --mx-muted --mx-border --mx-accent --mx-card --mx-bg) |
as cores do app, atualizadas com o tema e a cor de destaque |
Regras: apenas um arquivo (nenhum outro arquivo da pasta é entregue ao frame - coloque tudo inline, ou
carregue bibliotecas de um CDN com <script src="https://...">); sem localStorage/cookies (o sandbox não
tem: salve por meio do api.py e de ctx["state_dir"]). O botão de atualizar do cartão recarrega o
frame. Exemplo completo: examples/extensions/snake-widget/ no diretório do código.