Harness MaxxingDocumentation
📚 Toutes les pages

🧩 Étendre · 5 min de lecture

🧱Extensions de page

Une extension de page ajoute une page complète à l'app, servie à l'adresse /x/<id>. C'est un simple module JavaScript – sans étape de compilation – plus une API Python facultative.

Où elle apparaît : chaque page est listée sous Extensions dans la barre latérale (la page /x). Cliquez sur l'épingle en haut à droite d'une page (ou sur la page Extensions, ou activez Dans la barre latérale dans Paramètres -> Extensions) pour lui donner sa propre entrée dans la barre latérale, juste sous Extensions ; cliquez à nouveau dessus pour la détacher.

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

Demandez à Claude « crée une page qui... » et la compétence maxxing-extension l'écrit pour vous, ou suivez ces étapes.

Une nouvelle installation est fournie avec une page, Vos pages (extensions/pages/) : elle explique ce qu'est une extension de page et propose des exemples (fichier client, suivi de commandes, factures...) qui ouvrent une session avec la demande déjà saisie. C'est une extension comme les autres : supprimez son dossier, ou désactivez-la dans Paramètres -> Extensions, quand vous n'en avez plus besoin – les mises à jour ne la remettent pas.

1. manifest.json

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

name peut aussi être {"en": "Notes board", "fr": "Tableau de notes"} : l'app affiche celui de sa langue. "api": 1 (facultatif, 1 par défaut) est la version du contrat d'extension pour laquelle la page est écrite : si une prochaine version ne la prend plus en charge, la vérification préalable à la mise à jour vous le signale avant que vous ne mettiez à jour (Paramètres -> Système).

2. api.py (facultatif)

Chaque requête vers /api/ext/<id>/<path> appelle :

def handle(ctx, method, path, query, body):
    ...
    return {...}            # sent as JSON (200)
    # or: return 404, {"error": "not_found", "message": "..."}   to choose the status code
Argument Valeur
ctx identique à celui des widgets : id, workspace, state_dir, secret(name), config
method GET, POST, PUT, PATCH, DELETE
path la partie après /api/ext/<id>/ (par ex. notes, notes/42)
query les paramètres de la chaîne de requête sous forme de dictionnaire
body JSON analysé (ou texte), None pour GET ; 5 Mo maximum

Elle s'exécute dans un thread de travail avec une limite de 30 secondes. Les requêtes sont authentifiées par l'app : seul un utilisateur connecté peut l'appeler.

Exemple :

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 charge /ext/<id>/page.js et appelle mount(el, ctx) :

ctx
api(path, opts?) appelle /api/ext/<id>/<path> avec l'authentification et l'en-tête CSRF ; opts = {method, body} (le corps est envoyé en JSON) ; renvoie le JSON analysé, échoue en cas d'erreur HTTP
navigate(route) va vers une autre page de l'app, par ex. navigate("/tasks") ; /sessions?path=<topic path>&new=1&prompt=<text> ouvre une nouvelle session avec le texte déjà saisi
theme "dark" ou "light"
lang la langue de l'app, "en" ou "fr"
t(key) l'assistant de traduction de l'app
icon(name) une icône de l'app sous forme d'élément <svg> (mêmes noms que l'icon du manifeste), par ex. button.prepend(ctx.icon("plus"))

Renvoyez une fonction de nettoyage : elle s'exécute quand l'utilisateur quitte la page.

Par sécurité, l'app n'exécute que les scripts qu'elle sert elle-même : page.js ne peut pas ajouter de balises <script> en ligne à la page ni construire du code à partir de texte (eval) ; découpez plutôt le code en modules dans le dossier de l'extension. Les widgets HTML continuent de fonctionner comme avant.

Le module est téléchargé une seule fois par version de page.js et conservé : le code en haut du fichier s'exécute une fois, mount s'exécute à chaque visite. Enregistrer page.js lui donne une nouvelle version, prise en compte à la visite suivante.

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);
}

Mise en forme

La page bénéficie du kit d'interface de l'app : des classes CSS qui ressemblent exactement au reste de l'app et suivent d'elles-mêmes le thème, la couleur d'accent et la densité. Préférez-les aux styles écrits à la main :

Classes
ui-page ui-section ui-grid ui-row ui-stack ui-spacer mise en page : colonne de page, section, grille adaptative de cartes, ligne, colonne
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num texte (titre, titre de section, corps, secondaire, libellés, chiffres en colonnes)
ui-card ui-card-header ui-card-footer une carte ; l'en-tête accepte un ui-icon-box et un <div> avec le titre et une ligne de texte
ui-tile une carte cliquable : <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) une icône sur un carré doux de la couleur d'accent
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) boutons (ui-btn seul = action principale)
ui-field ui-label ui-input ui-select ui-textarea ui-check formulaires
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) statuts
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress données
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton étapes, avis, états vides et de chargement
ui-code ui-kbd ui-link ui-separator le reste

Les icônes viennent de ctx.icon(name) ; utilisez-les plutôt que des emoji. Votre propre CSS l'emporte toujours sur le kit, et les variables CSS de l'app sont là pour le reste : --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. Les widgets HTML disposent du même kit. Les champs du kit font 16 px sur téléphone (pas de zoom sur iPhone).

4. Tester et charger

  • Côté Python : appelez handle() directement (voir l'extrait de test dans Widgets du tableau de bord, avec api.handle(ctx, "GET", "notes", {}, None)).
  • Syntaxe JavaScript : node --check page.js si Node est installé.
  • Paramètres -> Extensions -> Recharger, puis ouvrez la page depuis la barre latérale (Extensions, ou sa propre entrée une fois épinglée). Les erreurs apparaissent sur la ligne de l'extension.
💡 Bloqué ? Ouvrez une session dans le topic system › help et posez votre question avec vos mots : Claude lit cette doc sur votre serveur et vous répond.✏️ Améliorer cette page