Harness MaxxingDocumentazione
📚 Tutte le pagine

🧩 Estendere · 5 min di lettura

🧱Estensioni pagina

Un'estensione pagina aggiunge un'intera pagina all'app, servita su /x/<id>. È un semplice modulo JavaScript – senza fase di build – più un'API Python opzionale.

Dove compare: ogni pagina è elencata sotto Estensioni nella barra laterale (la pagina /x). Clicca sulla puntina in alto a destra di una pagina (o nella pagina Estensioni, oppure attiva Nella barra laterale in Impostazioni -> Estensioni) per darle una voce dedicata nella barra laterale, subito sotto Estensioni; cliccala di nuovo per rimuoverla.

/root/maxxing/extensions/notes-board/
  manifest.json
  page.js        the page (ES module)
  api.py         optional: its HTTP API

Chiedi a Claude "crea una pagina che ..." e la skill maxxing-extension la scrive per te, oppure segui questi passaggi.

Una nuova installazione include una pagina, Le tue pagine (extensions/pages/): spiega che cos'è un'estensione pagina e mostra alcuni esempi (scheda cliente, tracciamento ordini, fatture...) che aprono una sessione con la richiesta già scritta. È un'estensione come le altre: elimina la sua cartella, oppure disattivala in Impostazioni -> Estensioni, quando non ti serve più - gli aggiornamenti non la reinstallano.

1. manifest.json

{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}

name può anche essere {"en": "Notes board", "fr": "Tableau de notes"}: l'app mostra quello della sua lingua. "api": 1 (opzionale, predefinito 1) è la versione del contratto delle estensioni per cui è scritta la pagina: se una versione futura smette di supportarla, il controllo preliminare dell'aggiornamento te lo segnala prima che tu aggiorni (Impostazioni -> Sistema).

2. api.py (opzionale)

Ogni richiesta a /api/ext/<id>/<path> chiama:

def handle(ctx, method, path, query, body):
    ...
    return {...}            # sent as JSON (200)
    # or: return 404, {"error": "not_found", "message": "..."}   to choose the status code
Argomento Valore
ctx uguale a quello dei widget: id, workspace, state_dir, secret(name), config
method GET, POST, PUT, PATCH, DELETE
path la parte dopo /api/ext/<id>/ (es. notes, notes/42)
query i parametri della query string come dizionario
body JSON già interpretato (o testo), None per GET; max 5 MB

Viene eseguita in un thread di lavoro con un limite di 30 secondi. Le richieste sono autenticate dall'app: solo un utente connesso può chiamarla.

Esempio:

import json
import time
from pathlib import Path


def handle(ctx, method, path, query, body):
    f = Path(ctx["state_dir"]) / "notes.json"
    notes = json.loads(f.read_text()) if f.exists() else []
    if path == "notes" and method == "GET":
        return {"notes": notes}
    if path == "notes" and method == "POST":
        text = str((body or {}).get("text", "")).strip()[:500]
        if text:
            notes.insert(0, {"text": text, "at": time.time()})
            tmp = f.with_suffix(".tmp")
            tmp.write_text(json.dumps(notes[:200]))
            tmp.replace(f)
        return {"notes": notes}
    return 404, {"error": "not_found", "message": f"{method} {path}"}

3. page.js

L'app carica /ext/<id>/page.js e chiama mount(el, ctx):

ctx
api(path, opts?) chiama /api/ext/<id>/<path> con autenticazione e header CSRF; opts = {method, body} (il body viene inviato come JSON); restituisce il JSON interpretato, viene rifiutata in caso di errori HTTP
navigate(route) va a un'altra pagina dell'app, es. navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> apre una nuova sessione con il testo già scritto
theme "dark" o "light"
lang la lingua dell'app, "en" o "fr"
t(key) funzione di traduzione dell'app
icon(name) un'icona dell'app come elemento <svg> (stessi nomi dell'icon del manifest), es. button.prepend(ctx.icon("plus"))

Restituisci una funzione di pulizia: viene eseguita quando l'utente lascia la pagina.

Per sicurezza l'app esegue solo gli script che serve lei stessa: page.js non può aggiungere tag <script> inline alla pagina né costruire codice a partire da un testo (eval); suddividi invece il codice in moduli nella cartella dell'estensione. I widget HTML continuano a funzionare come prima.

Il modulo viene scaricato una sola volta per ogni versione di page.js e conservato: il codice all'inizio del file viene eseguito una volta, mount a ogni visita. Salvando page.js si crea una nuova versione, che viene caricata alla visita successiva.

export function mount(el, ctx) {
  el.innerHTML = `
    <div class="ui-page" style="max-width:640px">
      <h1 class="ui-h1">Notes board</h1>
      <form class="ui-row">
        <input class="ui-input" name="text" placeholder="New note" style="flex:1">
        <button class="ui-btn">Add</button>
      </form>
      <ul class="ui-list"></ul>
      <p class="ui-muted" hidden>No note yet: write the first one above.</p>
    </div>`;
  const list = el.querySelector("ul");
  const empty = el.querySelector("p");
  const form = el.querySelector("form");
  form.querySelector("button").prepend(ctx.icon("plus"));
  const render = ({ notes }) => {
    list.hidden = !notes.length;
    empty.hidden = !!notes.length;
    list.replaceChildren(...notes.map((n) => {
      const li = document.createElement("li");
      li.textContent = n.text;            // never inject data with innerHTML
      return li;
    }));
  };
  ctx.api("notes").then(render);
  const onSubmit = async (e) => {
    e.preventDefault();
    const text = form.text.value.trim();
    if (!text) return;
    render(await ctx.api("notes", { method: "POST", body: { text } }));
    form.reset();
  };
  form.addEventListener("submit", onSubmit);
  return () => form.removeEventListener("submit", onSubmit);
}

Stile

La pagina riceve lo UI kit dell'app: classi CSS che hanno esattamente lo stesso aspetto del resto dell'app e seguono da sole il tema, il colore d'accento e la densità. Usale al posto di stili scritti a mano:

Classi
ui-page ui-section ui-grid ui-row ui-stack ui-spacer layout: colonna della pagina, sezione, griglia adattiva di card, riga, colonna
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num testo (titolo, titolo di sezione, corpo, secondario, etichette, cifre in colonna)
ui-card ui-card-header ui-card-footer una card; l'intestazione accetta un ui-icon-box e un <div> con il titolo e una riga
ui-tile una card cliccabile: <button class="ui-tile"><div class="ui-icon-box">svg</div><div><b>Title</b><span>line</span></div></button>
ui-icon-box (+ ui-lg, ui-neutral) un'icona su un quadrato tenue del colore d'accento
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) pulsanti (ui-btn da solo = azione principale)
ui-field ui-label ui-input ui-select ui-textarea ui-check moduli
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) stati
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress dati
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton passaggi, avvisi, stati vuoti e di caricamento
ui-code ui-kbd ui-link ui-separator il resto

Le icone provengono da ctx.icon(name); usale al posto delle emoji. Il tuo CSS ha sempre la precedenza sul kit, e le variabili CSS dell'app sono a disposizione per il resto: --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. I widget HTML ricevono lo stesso kit. Gli input del kit sono a 16 px sui telefoni (niente zoom su iPhone).

4. Prova e caricamento

  • Lato Python: chiama direttamente handle() (vedi l'esempio di test in Widget della dashboard, con api.handle(ctx, "GET", "notes", {}, None)).
  • Sintassi JavaScript: node --check page.js se Node è installato.
  • Impostazioni -> Estensioni -> Ricarica, poi apri la pagina dalla barra laterale (Estensioni, oppure la sua voce dedicata una volta fissata). Gli errori compaiono nella riga dell'estensione.
💡 Bloccato? Apri una sessione nel topic system › help e chiedi con parole tue: Claude legge questa documentazione sul tuo server e ti risponde.✏️ Migliora questa pagina