Dashboard widgets
The Dashboard is a grid of cards. Drag to reorder, hide the ones you do not need (layout menu), refresh a card with its refresh button.
Cards build themselves as they appear: numbers count up to their value, lists and tables rise in row by row, chart bars grow and lines draw, progress bars fill, and Today's schedule opens each job's line and drops its runs on it from left to right. A card further down the page waits until you scroll to it, so you see it too. When the notifications panel opens or closes, the cards slide to their new place. Everything stays still when your system asks for reduced motion.
A new install shows Activity, Server, Today's schedule, Tasks and Make it yours. The others are hidden until you need them (Edit layout shows them); Shopify appears by itself once a store is connected.
Make it yours is the card at the end: click an example ("Today's Shopify sales", "Visitors on my
website"...) and a new session opens in system/harness with the request already typed - send it, and
Harness writes the widget in your extensions. Any request works the same way from any session: "add a widget
that shows ...". Hide the card with Edit layout once you have yours.
Built-in widgets
| Widget | Shows |
|---|---|
| Activity | recent sessions, running turns, unread answers (before the first session: where to start) |
| Tasks | in progress, due today, overdue |
| Today's schedule | the day of your scheduled jobs on a 00:00 - 24:00 line: one row per job, runs done (green, red if failed), runs still to come (hollow), the one running now (pulsing) and a "now" line; hover a dot for its time and duration |
| Scheduled jobs | next runs and recent failures |
| Server | CPU, memory, disk, uptime, health issues |
| Memory backup | last run, age, repository |
| Telegram | bot and link status |
| Shopify | revenue today and over 7 days (vs the week before), orders today, orders to ship, 14-day revenue chart, latest orders — once a store is connected (below) |
| Make it yours | how to get your own widgets, with examples that open a session ready to send |
| Claude usage | off by default (Settings -> Features): plan usage windows, the same as the bars at the bottom of the sidebar (read from the last Claude answer; when none came for 10 minutes it asks an undocumented Anthropic endpoint, which may stop working; when that endpoint fails or asks to wait, the last figures read from Claude stay shown). |
Connect a Shopify store
The Shopify widget shows a "Connect your store" card until you add two keys:
- In Shopify admin: Settings → Apps and sales channels → Develop apps → Create an app, then
Configure Admin API scopes → tick
read_orders→ Install app → reveal the Admin API access token (shpat_…, shown once). - In Harness Maxxing: Settings → Keys → add
SHOPIFY_STORE=myshop.myshopify.comandSHOPIFY_ADMIN_TOKEN= the token.
The widget refreshes every 2 minutes (or with its refresh button) and only reads orders. Another shop (WooCommerce, Stripe, PrestaShop…)? Ask Claude for a widget: it builds an extension the same way (see below).
Writing your own widget
A widget is a folder in <workspace>/extensions/<id>/ with two files. You write data, not
HTML: the card is drawn by the app's design system, so it always looks right in dark and light
mode, on desktop and phone. (Need free-form drawing or interaction - a game, a canvas? See
HTML widgets below.)
The fastest way: ask Claude in a session, "create a dashboard widget that shows ...". The
maxxing-extension skill knows everything below.
1. Create the folder
/root/maxxing/extensions/disk-usage/
manifest.json
api.py
The id (folder name) is a lowercase slug: letters, digits, dashes. Reserved ids: 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}
| Key | Meaning |
|---|---|
type |
widget |
api |
version of the extension contract it is written for (default 1); a release that stops supporting it is announced by the update preflight before you update |
icon |
an icon name in kebab-case (gauge, chart-line, shopping-cart, users, receipt...) from the app's set of lucide icons; a name the app does not ship shows a puzzle piece |
span |
width in grid columns: 1, 2 or 3 |
ttl |
seconds the result is cached (default 300). The refresh button bypasses it. |
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) runs inside the server: use only the Python standard library, keep network calls short
(timeout ≤ 10 s). Extension code runs on 4 threads of its own: code that never returns (a network call without a
timeout) only blocks other extensions ("extensions are busy" until it returns), never the rest of Harness. An
api.py must load in under 10 s (no network call at import time), or the extension shows an error. ctx contains:
| Key | Value |
|---|---|
id |
the extension id |
workspace |
workspace path |
state_dir |
a folder reserved for this extension (cache, data) |
secret(name) |
value of a key from Settings -> Keys, or None |
config |
{"timezone", "language", "public_url"} |
Never hard-code a token: store it in Settings -> Keys and read it with ctx["secret"]("MY_TOKEN").
4. The Card schema
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 makes the card title clickable (an app route like /tasks or an https:// URL). Unknown
block types are ignored.
Example list + chart:
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. Test, then load
Test the function directly on the 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))"
Then Settings -> Extensions -> Reload. The widget appears on the dashboard. If something is
wrong (invalid manifest, import error, exception in card), the error is shown on the extension's
row and on the card; the rest of the dashboard keeps working.
Extensions live in your workspace: updates never touch them and the backup includes them.
HTML widgets (games, custom drawings)
When a Card is not enough - a small game, a canvas drawing, a custom chart library - write the
widget in HTML instead. Same folder, with widget.html instead of (or next to) api.py:
/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
The dashboard draws widget.html in a sandboxed frame: scripts run, but the frame cannot read
the app, its cookies or its API - a widget can never act as you. It talks to its own api.py
through a small bridge that is injected for you:
| In widget.html | What it does |
|---|---|
await maxxing.api("best") · maxxing.api("best", {method: "PUT", body: {...}}) |
calls handle() in this extension's api.py (/api/ext/<id>/best), returns its JSON |
maxxing.resize(400) |
sets the frame height in px (the manifest height, 120-800, is the default) |
maxxing.theme · maxxing.onTheme(fn) |
"dark" or "light", and a callback when it changes |
class="ui-card", ui-btn, ui-input, ui-badge, ui-stat... |
the app's UI kit (see Page extensions), already loaded in the frame |
var(--card) --foreground --muted-foreground --border --primary... (and the older --mx-fg --mx-muted --mx-border --mx-accent --mx-card --mx-bg) |
the app's colors, updated with the theme and the accent color |
Rules: one file only (no other file of the folder is served to the frame - inline everything, or
load libraries from a CDN <script src="https://...">); no localStorage/cookies (the sandbox has
none: save through api.py and ctx["state_dir"]). The refresh button of the card reloads the
frame. Full example: examples/extensions/snake-widget/ in the code directory.