🧩Widget della Dashboard
La Dashboard è una griglia di schede. Trascina per riordinarle, nascondi quelle che non ti servono (menu del layout), aggiorna una scheda con il suo pulsante di aggiornamento.
Le schede si costruiscono da sole mentre compaiono: i numeri salgono fino al loro valore, elenchi e tabelle appaiono riga per riga, le barre dei grafici crescono e le linee si disegnano, le barre di avanzamento si riempiono, e Programma di oggi apre la riga di ogni job e vi fa cadere sopra le sue esecuzioni da sinistra a destra. Una scheda più in basso nella pagina aspetta che tu ci arrivi con lo scorrimento, così la vedi anche tu. Quando il pannello delle notifiche si apre o si chiude, le schede scorrono verso la loro nuova posizione. Tutto resta fermo quando il tuo sistema chiede di ridurre le animazioni.
Una nuova installazione mostra Attività, Server, Programma di oggi, Attività da fare e Rendila tua. Le altre sono nascoste finché non ti servono (Modifica layout le mostra); Shopify compare da solo non appena un negozio è collegato.
Rendila tua è la scheda alla fine: clicca su un esempio ("Vendite Shopify di oggi", "Visitatori del mio
sito"...) e si apre una nuova sessione in system/harness con la richiesta già scritta - inviala, e
Harness scrive il widget nelle tue estensioni. Qualsiasi richiesta funziona allo stesso modo da qualsiasi sessione: "aggiungi un widget
che mostra ...". Nascondi la scheda con Modifica layout quando hai il tuo widget.
Widget integrati
| Widget | Cosa mostra |
|---|---|
| Attività | sessioni recenti, turni in corso, risposte non lette (prima della prima sessione: da dove iniziare) |
| Attività da fare | in corso, in scadenza oggi, in ritardo |
| Programma di oggi | la giornata dei tuoi job pianificati su una linea 00:00 - 24:00: una riga per job, esecuzioni completate (verdi, rosse se fallite), esecuzioni ancora da venire (vuote), quella in corso adesso (pulsante) e una linea "adesso"; passa il mouse su un punto per vederne l'orario e la durata |
| Job pianificati | prossime esecuzioni e fallimenti recenti |
| Server | CPU, memoria, disco, uptime, problemi di salute |
| Backup della memoria | ultima esecuzione, età, repository |
| Telegram | stato del bot e del collegamento |
| Shopify | ricavi di oggi e degli ultimi 7 giorni (rispetto alla settimana prima), ordini di oggi, ordini da spedire, grafico dei ricavi degli ultimi 14 giorni, ultimi ordini — una volta collegato un negozio (vedi sotto) |
| Rendila tua | come ottenere i tuoi widget, con esempi che aprono una sessione pronta da inviare |
| Utilizzo di Claude | disattivato di default (Impostazioni -> Funzionalità): finestre di utilizzo del piano, le stesse delle barre in fondo alla barra laterale (lette dall'ultima risposta di Claude; quando non ne arriva nessuna per 10 minuti, interroga un endpoint Anthropic non documentato, che potrebbe smettere di funzionare; quando quell'endpoint fallisce o chiede di attendere, restano visibili gli ultimi dati letti da Claude). |
Collegare un negozio Shopify
Il widget Shopify mostra una scheda "Collega il tuo negozio" finché non aggiungi due chiavi:
- Nell'admin di Shopify: Impostazioni → App e canali di vendita → Sviluppa app → Crea un'app, poi
Configura gli ambiti dell'API Admin → seleziona
read_orders→ Installa app → mostra il token di accesso dell'API Admin (shpat_…, mostrato una sola volta). - In Harness Maxxing: Impostazioni → Chiavi → aggiungi
SHOPIFY_STORE=myshop.myshopify.comeSHOPIFY_ADMIN_TOKEN= il token.
Il widget si aggiorna ogni 2 minuti (o con il suo pulsante di aggiornamento) e legge soltanto gli ordini. Hai un altro negozio (WooCommerce, Stripe, PrestaShop…)? Chiedi a Claude un widget: costruisce un'estensione nello stesso modo (vedi sotto).
Scrivere il tuo widget
Un widget è una cartella in <workspace>/extensions/<id>/ con due file. Scrivi dati, non
HTML: la scheda viene disegnata dal design system dell'app, quindi ha sempre un bell'aspetto in modalità scura e chiara,
su desktop e su telefono. (Ti serve un disegno libero o un'interazione - un gioco, un canvas? Vedi
Widget HTML più sotto.)
Il modo più veloce: chiedi a Claude in una sessione, "crea un widget per la dashboard che mostra ...". La
skill maxxing-extension conosce tutto quello che segue.
1. Crea la cartella
/root/maxxing/extensions/disk-usage/
manifest.json
api.py
L'id (il nome della cartella) è uno slug in minuscolo: lettere, cifre, trattini. Id riservati: 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}
| Chiave | Significato |
|---|---|
type |
widget |
api |
versione del contratto delle estensioni per cui è scritta (predefinita 1); una versione che smette di supportarla viene annunciata dal controllo preliminare dell'aggiornamento prima che tu aggiorni |
icon |
il nome di un'icona in kebab-case (gauge, chart-line, shopping-cart, users, receipt...) tra le icone lucide incluse nell'app; un nome che l'app non include mostra un pezzo di puzzle |
span |
larghezza in colonne della griglia: 1, 2 o 3 |
ttl |
secondi di permanenza del risultato nella cache (predefinito 300). Il pulsante di aggiornamento la 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) viene eseguita dentro il server: usa solo la libreria standard di Python e tieni brevi le chiamate di rete
(timeout ≤ 10 s). Il codice delle estensioni gira su 4 thread dedicati: un codice che non ritorna mai (una chiamata di rete senza
timeout) blocca soltanto le altre estensioni ("extensions are busy" finché non ritorna), mai il resto di Harness. Un
api.py deve caricarsi in meno di 10 s (nessuna chiamata di rete al momento dell'import), altrimenti l'estensione mostra un errore. ctx contiene:
| Chiave | Valore |
|---|---|
id |
l'id dell'estensione |
workspace |
percorso del workspace |
state_dir |
una cartella riservata a questa estensione (cache, dati) |
secret(name) |
valore di una chiave di Impostazioni -> Chiavi, oppure None |
config |
{"timezone", "language", "public_url"} |
Non scrivere mai un token direttamente nel codice: salvalo in Impostazioni -> Chiavi e leggilo con ctx["secret"]("MY_TOKEN").
4. Lo schema della 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 rende cliccabile il titolo della scheda (una rotta dell'app come /tasks o un URL https://). I tipi di
blocco sconosciuti vengono ignorati.
Esempio di elenco + grafico:
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. Prova, poi carica
Prova la funzione direttamente sul server:
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))"
Poi Impostazioni -> Estensioni -> Ricarica. Il widget compare nella dashboard. Se qualcosa
non va (manifest non valido, errore di import, eccezione in card), l'errore viene mostrato sulla riga dell'estensione
e sulla scheda; il resto della dashboard continua a funzionare.
Le estensioni vivono nel tuo workspace: gli aggiornamenti non le toccano mai e il backup le include.
Widget HTML (giochi, disegni personalizzati)
Quando una Card non basta - un piccolo gioco, un disegno su canvas, una libreria di grafici personalizzata - scrivi il
widget in HTML. Stessa cartella, con widget.html al posto di api.py (o accanto):
/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
La dashboard disegna widget.html in un frame isolato (sandbox): gli script vengono eseguiti, ma il frame non può leggere
l'app, i suoi cookie o la sua API - un widget non può mai agire al posto tuo. Comunica con il proprio api.py
attraverso un piccolo ponte che viene iniettato per te:
| In widget.html | Cosa fa |
|---|---|
await maxxing.api("best") · maxxing.api("best", {method: "PUT", body: {...}}) |
chiama handle() nell'api.py di questa estensione (/api/ext/<id>/best), restituisce il suo JSON |
maxxing.resize(400) |
imposta l'altezza del frame in px (il height del manifest, 120-800, è il valore predefinito) |
maxxing.theme · maxxing.onTheme(fn) |
"dark" o "light", e una callback quando cambia |
class="ui-card", ui-btn, ui-input, ui-badge, ui-stat... |
l'UI kit dell'app (vedi Estensioni di pagina), già caricato nel frame |
var(--card) --foreground --muted-foreground --border --primary... (e i più vecchi --mx-fg --mx-muted --mx-border --mx-accent --mx-card --mx-bg) |
i colori dell'app, aggiornati con il tema e il colore d'accento |
Regole: un solo file (nessun altro file della cartella viene servito al frame - metti tutto inline, oppure
carica le librerie da una CDN <script src="https://...">); niente localStorage/cookie (la sandbox non ne
ha: salva tramite api.py e ctx["state_dir"]). Il pulsante di aggiornamento della scheda ricarica il
frame. Esempio completo: examples/extensions/snake-widget/ nella cartella del codice.