🧩Widgets du tableau de bord
Le Tableau de bord est une grille de cartes. Faites-les glisser pour les réorganiser, masquez celles dont vous n'avez pas besoin (menu de disposition), actualisez une carte avec son bouton d'actualisation.
Les cartes se construisent au moment où elles apparaissent : les chiffres montent jusqu'à leur valeur, les listes et les tableaux apparaissent ligne par ligne, les barres des graphiques grandissent et les courbes se tracent, les barres de progression se remplissent, et Planning du jour ouvre la ligne de chaque tâche et y dépose ses exécutions de gauche à droite. Une carte située plus bas dans la page attend que vous y arriviez en faisant défiler, pour que vous en voyiez aussi l'animation. Quand le panneau des notifications s'ouvre ou se ferme, les cartes glissent vers leur nouvelle place. Tout reste immobile lorsque votre système demande de réduire les animations.
Une nouvelle installation affiche Activité, Serveur, Planning du jour, Tâches et Personnalisez votre espace. Les autres sont masquées jusqu'à ce que vous en ayez besoin (Modifier la disposition les affiche) ; Shopify apparaît tout seul dès qu'une boutique est connectée.
Personnalisez votre espace est la carte placée à la fin : cliquez sur un exemple (« Ventes Shopify du jour », « Visiteurs sur mon site »...) et une nouvelle session s'ouvre dans system/harness avec la demande déjà saisie - envoyez-la, et Harness écrit le widget dans vos extensions. N'importe quelle demande fonctionne de la même façon depuis n'importe quelle session : « ajoute un widget qui affiche ... ». Masquez la carte avec Modifier la disposition une fois que vous avez le vôtre.
Widgets intégrés
| Widget | Affiche |
|---|---|
| Activité | les sessions récentes, les tours en cours, les réponses non lues (avant la première session : par où commencer) |
| Tâches | en cours, à faire aujourd'hui, en retard |
| Planning du jour | la journée de vos tâches planifiées sur une ligne de 00:00 à 24:00 : une rangée par tâche, les exécutions terminées (en vert, en rouge en cas d'échec), les exécutions à venir (creuses), celle en cours (qui pulse) et une ligne « maintenant » ; survolez un point pour voir son heure et sa durée |
| Tâches planifiées | les prochaines exécutions et les échecs récents |
| Serveur | processeur, mémoire, disque, durée de fonctionnement, problèmes de santé |
| Sauvegarde de la mémoire | dernière exécution, ancienneté, dépôt |
| Telegram | état du bot et de la liaison |
| Shopify | chiffre d'affaires du jour et sur 7 jours (par rapport à la semaine précédente), commandes du jour, commandes à expédier, graphique du chiffre d'affaires sur 14 jours, dernières commandes — une fois qu'une boutique est connectée (voir ci-dessous) |
| Personnalisez votre espace | comment obtenir vos propres widgets, avec des exemples qui ouvrent une session prête à envoyer |
| Utilisation de Claude | désactivé par défaut (Paramètres -> Fonctionnalités) : les fenêtres d'utilisation de votre forfait, identiques aux barres en bas de la barre latérale (lues dans la dernière réponse de Claude ; quand aucune n'est arrivée depuis 10 minutes, il interroge un point d'accès Anthropic non documenté, qui peut cesser de fonctionner ; quand ce point d'accès échoue ou demande d'attendre, les derniers chiffres lus dans Claude restent affichés). |
Connecter une boutique Shopify
Le widget Shopify affiche une carte « Connectez votre boutique » tant que vous n'avez pas ajouté deux clés :
- Dans l'administration Shopify : Paramètres → Applications et canaux de vente → Développer des applications → Créer une application, puis Configurer les portées de l'API Admin → cochez
read_orders→ Installer l'application → affichez le jeton d'accès de l'API Admin (shpat_…, affiché une seule fois). - Dans Harness Maxxing : Paramètres → Clés → ajoutez
SHOPIFY_STORE=myshop.myshopify.cometSHOPIFY_ADMIN_TOKEN= le jeton.
Le widget s'actualise toutes les 2 minutes (ou avec son bouton d'actualisation) et ne fait que lire les commandes. Une autre boutique (WooCommerce, Stripe, PrestaShop…) ? Demandez un widget à Claude : il construit une extension de la même façon (voir ci-dessous).
Écrire votre propre widget
Un widget est un dossier dans <workspace>/extensions/<id>/ contenant deux fichiers. Vous écrivez des données, pas du HTML : la carte est dessinée par le système de design de l'app, elle est donc toujours belle en mode sombre comme en mode clair, sur ordinateur comme sur téléphone. (Besoin d'un dessin libre ou d'une interaction - un jeu, un canvas ? Voir Widgets HTML ci-dessous.)
Le plus rapide : demandez à Claude dans une session, « crée un widget de tableau de bord qui affiche ... ». La compétence maxxing-extension connaît tout ce qui suit.
1. Créer le dossier
/root/maxxing/extensions/disk-usage/
manifest.json
api.py
L'identifiant (le nom du dossier) est un slug en minuscules : lettres, chiffres, tirets. Identifiants réservés : 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}
| Clé | Signification |
|---|---|
type |
widget |
api |
version du contrat d'extension pour laquelle il est écrit (1 par défaut) ; une version qui cesse de la prendre en charge est annoncée par la vérification préalable à la mise à jour, avant que vous ne mettiez à jour |
icon |
un nom d'icône en kebab-case (gauge, chart-line, shopping-cart, users, receipt...) parmi les icônes lucide fournies avec l'app ; un nom que l'app ne fournit pas affiche une pièce de puzzle |
span |
largeur en colonnes de la grille : 1, 2 ou 3 |
ttl |
nombre de secondes pendant lesquelles le résultat est mis en cache (300 par défaut). Le bouton d'actualisation l'ignore. |
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) s'exécute à l'intérieur du serveur : utilisez uniquement la bibliothèque standard de Python et gardez les appels réseau courts (délai d'attente ≤ 10 s). Le code des extensions s'exécute sur 4 threads qui leur sont propres : un code qui ne rend jamais la main (un appel réseau sans délai d'attente) ne bloque que les autres extensions (« les extensions sont occupées » jusqu'à ce qu'il rende la main), jamais le reste de Harness. Un api.py doit se charger en moins de 10 s (aucun appel réseau à l'import), sinon l'extension affiche une erreur. ctx contient :
| Clé | Valeur |
|---|---|
id |
l'identifiant de l'extension |
workspace |
le chemin de l'espace de travail |
state_dir |
un dossier réservé à cette extension (cache, données) |
secret(name) |
la valeur d'une clé de Paramètres -> Clés, ou None |
config |
{"timezone", "language", "public_url"} |
N'écrivez jamais un jeton en dur : enregistrez-le dans Paramètres -> Clés et lisez-le avec ctx["secret"]("MY_TOKEN").
4. Le schéma de 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 rend le titre de la carte cliquable (une route de l'app comme /tasks ou une URL https://). Les types de blocs inconnus sont ignorés.
Exemple de liste + graphique :
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. Tester, puis charger
Testez la fonction directement sur le serveur :
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))"
Puis Paramètres -> Extensions -> Recharger. Le widget apparaît sur le tableau de bord. Si quelque chose ne va pas (manifeste invalide, erreur d'import, exception dans card), l'erreur s'affiche sur la ligne de l'extension et sur la carte ; le reste du tableau de bord continue de fonctionner.
Les extensions vivent dans votre espace de travail : les mises à jour n'y touchent jamais et la sauvegarde les inclut.
Widgets HTML (jeux, dessins personnalisés)
Quand une Card ne suffit pas - un petit jeu, un dessin sur canvas, une bibliothèque de graphiques personnalisée - écrivez plutôt le widget en HTML. Même dossier, avec widget.html à la place de api.py (ou en plus) :
/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
Le tableau de bord affiche widget.html dans un cadre isolé (sandbox) : les scripts s'exécutent, mais le cadre ne peut pas lire l'app, ses cookies ni son API - un widget ne peut donc jamais agir à votre place. Il communique avec son propre api.py grâce à un petit pont qui est injecté pour vous :
| Dans widget.html | Ce que ça fait |
|---|---|
await maxxing.api("best") · maxxing.api("best", {method: "PUT", body: {...}}) |
appelle handle() dans le api.py de cette extension (/api/ext/<id>/best) et renvoie son JSON |
maxxing.resize(400) |
règle la hauteur du cadre en px (la valeur height du manifeste, 120-800, est la valeur par défaut) |
maxxing.theme · maxxing.onTheme(fn) |
"dark" ou "light", et une fonction de rappel quand il change |
class="ui-card", ui-btn, ui-input, ui-badge, ui-stat... |
le kit d'interface de l'app (voir Extensions de page), déjà chargé dans le cadre |
var(--card) --foreground --muted-foreground --border --primary... (et les anciennes --mx-fg --mx-muted --mx-border --mx-accent --mx-card --mx-bg) |
les couleurs de l'app, mises à jour avec le thème et la couleur d'accent |
Règles : un seul fichier (aucun autre fichier du dossier n'est servi au cadre - mettez tout en ligne, ou chargez les bibliothèques depuis un CDN avec <script src="https://...">) ; pas de localStorage ni de cookies (le bac à sable n'en a pas : enregistrez via api.py et ctx["state_dir"]). Le bouton d'actualisation de la carte recharge le cadre. Exemple complet : examples/extensions/snake-widget/ dans le dossier du code.