Harness MaxxingDoku
📚 Alle Seiten

🧩 Erweitern · 5 Min. Lesezeit

🧱Seiten-Erweiterungen

Eine Seiten-Erweiterung fügt der App eine vollständige Seite hinzu, die unter /x/<id> bereitgestellt wird. Sie ist ein einfaches JavaScript-Modul – ohne Build-Schritt – plus eine optionale Python-API.

Wo sie erscheint: Jede Seite steht in der Seitenleiste unter Erweiterungen (die Seite /x). Klicke auf die Pinnnadel oben rechts auf einer Seite (oder auf der Seite Erweiterungen, oder aktiviere In der Seitenleiste unter Einstellungen -> Erweiterungen), damit sie einen eigenen Eintrag in der Seitenleiste direkt unter Erweiterungen bekommt; klicke erneut darauf, um sie zu lösen.

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

Bitte Claude: „Erstelle eine Seite, die ..." und der Skill maxxing-extension schreibt sie für dich, oder folge diesen Schritten.

Eine neue Installation enthält eine Seite, Deine Seiten (extensions/pages/): Sie erklärt, was eine Seiten-Erweiterung ist, und zeigt Beispiele (Kundendatei, Bestellverfolgung, Rechnungen ...), die eine Session mit bereits eingetippter Anfrage öffnen. Sie ist eine ganz normale Erweiterung: Lösche ihren Ordner oder schalte sie unter Einstellungen -> Erweiterungen aus, wenn du sie nicht mehr brauchst – Updates bringen sie nicht zurück.

1. manifest.json

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

name kann auch {"en": "Notes board", "fr": "Tableau de notes"} sein: Die App zeigt den Namen in ihrer Sprache an. "api": 1 (optional, Standard 1) ist die Version des Erweiterungsvertrags, für die die Seite geschrieben ist: Wenn eine künftige Version sie nicht mehr unterstützt, weist dich der Update-Vorabcheck vor dem Update darauf hin (Einstellungen -> System).

2. api.py (optional)

Jede Anfrage an /api/ext/<id>/<path> ruft Folgendes auf:

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 Wert
ctx wie bei Widgets: id, workspace, state_dir, secret(name), config
method GET, POST, PUT, PATCH, DELETE
path der Teil nach /api/ext/<id>/ (z. B. notes, notes/42)
query Query-String-Parameter als Dict
body geparstes JSON (oder Text), None bei GET; max. 5 MB

Sie läuft in einem Worker-Thread mit einem Limit von 30 Sekunden. Die Anfragen werden von der App authentifiziert: Nur ein angemeldeter Nutzer kann sie aufrufen.

Beispiel:

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

Die App lädt /ext/<id>/page.js und ruft mount(el, ctx) auf:

ctx
api(path, opts?) ruft /api/ext/<id>/<path> mit Authentifizierung und CSRF-Header auf; opts = {method, body} (der Body wird als JSON gesendet); liefert das geparste JSON zurück und schlägt bei HTTP-Fehlern fehl
navigate(route) wechselt zu einer anderen Seite der App, z. B. navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> öffnet eine neue Session mit dem eingetippten Text
theme "dark" oder "light"
lang die Sprache der App, "en" oder "fr"
t(key) Übersetzungshilfe der App
icon(name) ein Icon der App als <svg>-Element (dieselben Namen wie beim icon im Manifest), z. B. button.prepend(ctx.icon("plus"))

Gib eine Aufräumfunktion zurück: Sie läuft, wenn der Nutzer die Seite verlässt.

Aus Sicherheitsgründen führt die App nur Skripte aus, die sie selbst ausliefert: page.js kann der Seite keine Inline-<script>-Tags hinzufügen und keinen Code aus Text erzeugen (eval); teile den Code stattdessen in Module im Ordner der Erweiterung auf. HTML-Widgets funktionieren weiterhin wie bisher.

Das Modul wird einmal pro Version von page.js heruntergeladen und gespeichert: Code am Anfang der Datei läuft einmal, mount läuft bei jedem Besuch. Wenn du page.js speicherst, erhält sie eine neue Version, die beim nächsten Besuch übernommen wird.

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

Gestaltung

Die Seite erhält das UI-Kit der App: CSS-Klassen, die genau wie der Rest der App aussehen und von selbst dem Theme, der Akzentfarbe und der Dichte folgen. Bevorzuge sie gegenüber selbst geschriebenen Styles:

Klassen
ui-page ui-section ui-grid ui-row ui-stack ui-spacer Layout: Seitenspalte, Abschnitt, responsives Raster aus Karten, Zeile, Spalte
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num Text (Titel, Abschnittstitel, Fließtext, Sekundärtext, Beschriftungen, Zahlen in Spalten)
ui-card ui-card-header ui-card-footer eine Karte; der Header nimmt eine ui-icon-box und ein <div> mit dem Titel und einer Zeile auf
ui-tile eine anklickbare Karte: <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) ein Icon auf einem weichen Quadrat in der Akzentfarbe
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) Schaltflächen (ui-btn allein = Hauptaktion)
ui-field ui-label ui-input ui-select ui-textarea ui-check Formulare
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) Status
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress Daten
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton Schritte, Hinweise, leere Zustände und Ladezustände
ui-code ui-kbd ui-link ui-separator der Rest

Icons kommen von ctx.icon(name); verwende sie statt Emojis. Dein eigenes CSS gewinnt immer gegen das Kit, und die CSS-Variablen der App stehen für alles Weitere bereit: --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. HTML-Widgets erhalten dasselbe Kit. Die Eingabefelder des Kits haben auf Smartphones 16 px (kein Zoomen auf dem iPhone).

4. Testen und laden

  • Python-Seite: Rufe handle() direkt auf (siehe das Test-Snippet in Dashboard-Widgets, mit api.handle(ctx, "GET", "notes", {}, None)).
  • JavaScript-Syntax: node --check page.js, wenn Node installiert ist.
  • Einstellungen -> Erweiterungen -> Neu laden, öffne dann die Seite über die Seitenleiste (Erweiterungen oder ihren eigenen Eintrag, sobald sie angepinnt ist). Fehler erscheinen in der Zeile der Erweiterung.
💡 Nicht weiter? Öffne eine Session im Topic system › help und frag in deinen eigenen Worten: Claude liest diese Doku auf deinem Server und antwortet.✏️ Diese Seite verbessern