Live-Dashboard für AI Agents: Anleitung zum Selbstbauen
Wer viele AI-Automatisierungen und Agent-Teams betreibt, kann deren Aktivitäten auf einem eigenen Live-Dashboard überwachen. Ein kostenloser Starter-Code dazu ist auf GitHub verfügbar.

Veröffentlicht am 9. Oktober • Zuerst erschienen auf garystow.co.uk
Zuerst veröffentlicht auf garystow.co.uk. Der kostenlose Startercode befindet sich auf GitHub.
Geschrieben von Gary Stow mit Unterstützung durch KI-Tools. Jede Faktenangabe wird geprüft und jeder Artikel wird von Gary freigegeben, bevor er online geht.
Ich betreibe eine ganze Reihe von Automatisierungen. Ein Team schreibt und prüft Artikel für diese Website, ein anderes führt drei Demo-Handelsstrategien aus, und ein Supervisor sitzt über beiden und entscheidet, was meine Aufmerksamkeit braucht. Monatelang war die einzige Möglichkeit, irgendetwas davon zu sehen, ein Dashboard voller Tabellen. Für mich funktionierte das, aber wenn meine Frau oder meine Kinder fragten, was ich eigentlich jeden Abend baute, bedeutete eine Tabelle mit Jobnamen ihnen nichts.
Also habe ich das Ganze in ein Dorf verwandelt. Ich habe es gebaut, um meine Arbeit meiner Familie zu erklären, und der visuelle Ansatz hat es ihnen viel leichter verständlich gemacht als alles, was ich hätte sagen können. Der Stil ist eine Mischung aus RimWorld und South Park: Ich spiele gerne RimWorld und bin seit jeher Fan von South Park.
Jede Seite dieser Website ist ein Haus. Jede Handelsstrategie ist ein Feld, das mit seinen Ergebnissen wächst oder verdorrt. Jede Anfrage läuft zum Rathaus, und Bauarbeiter arbeiten ständig am nächsten Haus. Die Live-Version können Sie im Village sehen.
Live-Village öffnen ↗ So funktioniert das Dorf
Dieser Leitfaden zeigt Ihnen Schritt für Schritt, wie Sie Ihr eigenes bauen. Sie brauchen keine Game-Engine, und alles hier ist kostenlos. Wenn Sie ein Python-Skript ausführen können, können Sie das schaffen.
Es gibt nur drei bewegliche Teile, und sie getrennt zu halten macht das Ganze beherrschbar.
Die Zeichnung spricht nie direkt mit Ihren Automatisierungen. Das bedeutet, dass ein Fehler im Dorf nie die echte Arbeit beeinträchtigen kann und Sie das Bild so oft neu erstellen können, wie Sie möchten.
Das Dorf kann nur zeigen, was Ihre Automatisierungen aufzeichnen. Geben Sie jedem Skript eine gemeinsame Funktion, die eine Zeile in ein Aktivitätsprotokoll schreibt. Eine JSON-Zeile pro Ereignis, genannt JSON Lines, ist das einfachste Format überhaupt, weil man immer nur ans Ende der Datei anhängt.
import json from datetime import datetime def log_activity ( who, text, outcome = " ok " ): row = { " ts ": datetime. now (). strftime ( " %Y-%m-%d %H:%M " ), " who ": who, " text ": text, " outcome ": outcome } with open ( " activity_log.jsonl ", " a ", encoding = " utf-8 " ) as f: f. write ( json. dumps ( row ) + " \n " ) log_activity ( " writer ", " Drafted: how to build a village " ) Vollbildmodus aktivieren Vollbildmodus beenden Rufen Sie die Funktion zu den wichtigen Momenten auf: wenn ein Job startet, endet, fehlschlägt oder auf einen Menschen wartet.
Bevor Sie irgendetwas zeichnen, schreiben Sie auf, wofür jedes Gebäude steht. Das ist der Teil, der das Dorf für jemanden verständlich macht, der Ihr System noch nie gesehen hat. Meins sieht so aus:
Führen Sie eine einfache Zuordnung von den Namen in Ihrem Protokoll zu dem Gebäude, in dem diese Arbeit passiert.
Speichern Sie das als village_data.py. Es liest die letzten 48 Stunden des Protokolls und schreibt die Datei, aus der die Seite ihre Daten bezieht. Führen Sie es nach einem Zeitplan aus oder jedes Mal, wenn die Seite danach fragt (Schritt 9 macht das).
import json from datetime import datetime, timedelta from pathlib import Path LOG = Path ( " activity_log.jsonl " ) OUT = Path ( " village/village_data.json " ) BUILDING_FOR = { " writer ": " workshop ", " checker ": " hall ", " publisher ": " site ", " trader ": " field " } def recent_events ( hours = 48 ): cutoff = datetime. now () - timedelta ( hours = hours ) events = [] for line in LOG. read_text ( encoding = " utf-8 " ). splitlines (): e = json. loads ( line ) if datetime. strptime ( e [ " ts " ], " %Y-%m-%d %H:%M " ) >= cutoff: events. append ({ " time ": e [ " ts " ][ 11:], " from ": " hall ", # jeder Job startet am Rathaus " at ": BUILDING_FOR. get ( e [ " who " ], " hall " ), " text ": e [ " text " ]}) return events [ - 30:] def build ( pages, results ): data = { " generated ": datetime. now (). isoformat ( timespec = " minutes " ), " pages ": pages, " events ": recent_events (), " fields ": results } OUT. parent. mkdir ( exist_ok = True ) OUT. write_text ( json. dumps ( data ), encoding = " utf-8 " ) def main (): build ( pages = [{ " title ": " Home page ", " url ": " / ", " views ": 84 }], results = { " strategy_a ": 120, " strategy_b ": - 40 }) if __name__ == " __main__ ": main () Vollbildmodus aktivieren Vollbildmodus beenden Die Seitenaufrufe können aus jedem Analytics-Export stammen, den Sie bereits haben. Beginnen Sie mit ausgedachten Zahlen und ersetzen Sie sie durch echte, sobald das Bild funktioniert.
Der RimWorld-artige Look entsteht durch isometrische Kacheln: Rauten, die doppelt so breit wie hoch sind. Eine kleine Funktion wandelt ein Gitterquadrat in eine Position auf dem Bildschirm um, und alles andere baut darauf auf.
const TW = 64, TH = 32; // eine Kachel: 64 breit, 32 hoch function toScreen ( x, y ) { // Gitterquadrat -> Pixelposition return { sx: ( x - y ) * TW / 2, sy: ( x + y ) * TH / 2 }; } function tile ( ctx, x, y, colour ) { const { sx, sy } = toScreen ( x, y ); ctx. beginPath (); ctx. moveTo ( sx, sy ); ctx. lineTo ( sx + TW / 2, sy + TH / 2 ); ctx. lineTo ( sx, sy + TH ); ctx. lineTo ( sx - TW / 2, sy + TH / 2 ); ctx. closePath (); ctx. fillStyle = colour; ctx. fill (); } Vollbildmodus aktivieren Vollbildmodus beenden Schleifen Sie über ein Gitter und rufen Sie tile für jedes Quadrat auf. Verwenden Sie eine Farbe für Gras, eine andere für Straßen und Blau für einen Fluss zwischen den Vierteln.
Ein Gebäude besteht aus drei Formen: einer dunkleren linken Wand, einer helleren rechten Wand und einem Dach. Dicke schwarze Umrandungen verleihen den ausgestanzten Look. Zeichnen Sie vom hinteren Teil der Karte zum vorderen, sortiert nach x + y, damit nähere Gebäude die dahinterliegenden verdecken.
function poly ( ctx, pts, fill ) { ctx. beginPath (); pts. forEach (([ px, py ], i ) => i? ctx. lineTo ( px, py ): ctx. moveTo ( px, py )); ctx. closePath (); ctx. fillStyle = fill; ctx. fill (); ctx. lineWidth = 3; ctx. strokeStyle = " #000 "; ctx. stroke (); } function shade ( hex, pct ) { // eine Farbe aufhellen oder abdunkeln const n = parseInt ( hex. slice ( 1 ), 16 ); const f = c => Math. max ( 0, Math. min ( 255, c + Math. round ( 2.55 * pct ))); return " # " + [ f ( n >> 16 ), f (( n >> 8 ) & 255 ), f ( n & 255 )]. map ( v => v. toString ( 16 ). padStart ( 2, " 0 " )). join ( "" ); } function box ( ctx, x, y, h, colour ) { // h = Höhe in Pixeln const { sx, sy } = toScreen ( x, y ); const top = sy - h; poly ( ctx, [[ sx - TW / 2, top + TH / 2 ], [ sx, top + TH ], [ sx, sy + TH ], [ sx - TW / 2, sy + TH / 2 ]], shade ( colour, - 25 )); poly ( ctx, [[ sx, top + TH ], [ sx + TW / 2, top + TH / 2 ], [ sx + TW / 2, sy + TH / 2 ], [ sx, sy + TH ]], shade ( colour, - 10 )); poly ( ctx, [[ sx, top ], [ sx + TW / 2, top + TH / 2 ], [ sx, top + TH ], [ sx - TW / 2, top + TH / 2 ]], colour ); } Vollbildmodus aktivieren Vollbildmodus beenden Fügen Sie Fenster, Türen und Schilder hinzu, sobald die Boxen stimmig aussehen. Widerstehen Sie dem Drang, zu früh alles hübsch zu machen. Bringen Sie zuerst das Layout mit schlichten Boxen zum Laufen.
Das ist der Teil, der das Dorf zum Leben erweckt. Jedes Ereignis in der Datendatei schickt eine kleine Person von einem Gebäude zum anderen. Wenn der Supervisor etwas freigibt, geht jemand vom Rathaus zum Executor. Wenn ein Artikel online geht, läuft ein Bauarbeiter zum Wohnviertel.
const walkers = []; function playEvent ( ev ) { // ev.from und ev.at sind Gebäudenamen walkers. push ({ from: BUILDINGS [ ev. from ], to: BUILDINGS [ ev. at ], t: 0 }); } function moveWalkers ( dt ) { // dt = Millisekunden seit dem letzten Frame for ( const w of walkers ) w. t = Math. min ( 1, w. t + dt / 4000 ); // 4 Sekunden pro Weg for ( let i = walkers. length - 1; i >= 0; i -- ) { if ( walkers [ i ]. t >= 1 ) walkers. splice ( i, 1 ); } } function walkerPos ( w ) { return { x: w. from. x + ( w. to. x - w. from. x ) * w. t, y: w. from. y + ( w. to. y - w. from. y ) * w. t }; } Vollbildmodus aktivieren Vollbildmodus beenden Spielen Sie alle paar Sekunden ein Ereignis ab und springen Sie zum Anfang zurück, wenn Ihnen welche ausgehen. Zeichnen Sie einen farbigen Pfad für den Ablauf jedes Teams, damit Sie auf einen Blick sehen, dass Website-Arbeit und Handelsarbeit sich nie kreuzen.
Jetzt verbinden Sie das Bild mit echten Zahlen. Diese beiden Regeln erledigen in meinem Dorf die meiste Arbeit.
function fieldScale ( net ) { // net = Gewinn oder Verlust in Pfund return Math. max ( 0.6, Math. min ( 1.45, 1 + net / 400 )); } function houseFloors ( views ) { return views > 200? 3: views > 50? 2: 1; } Vollbildmodus aktivieren Vollbildmodus beenden Das Begrenzen ist wichtig. Ohne diese Grenzen würde eine einzige großartige oder schreckliche Woche dazu führen, dass ein Feld den ganzen Bildschirm füllt oder ganz verschwindet. Wählen Sie Grenzen, die das Dorf lesbar halten.
Speichern Sie die Seite als village/index.html, neben der Datendatei, mit einem <canvas id="village" width="1200" height="750"> darin. Rufen Sie die Daten einmal ab und zeichnen Sie dann etwa sechzigmal pro Sekunde neu.
const canvas = document.getElementById("village"); const ctx = canvas.getContext("2d"); let DATA = null, last = performance.now(); function frame(now) { const dt = now - last; last = now; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.setTransform(1, 0, 0, 1, canvas.width / 2, 80); // Grid-Feld 0,0 sitzt oben mittig drawGround(); drawBuildings(); moveWalkers(dt); drawWalkers(); requestAnimationFrame(frame); } fetch("village_data.json").then(r => r.json()).then(d => { DATA = d; requestAnimationFrame(frame); }); Enter fullscreen mode Exit fullscreen mode drawGround, drawBuildings und drawWalkers sind eigene Funktionen, die du aus den obigen Bausteinen zusammenbaust.
Mein privates Dorf läuft über dieselbe kleine Flask-App wie mein lokales Dashboard, daher kann es alles anzeigen, einschließlich der tatsächlichen Anfragen, die auf mich warten. Klickt man auf eine Person außerhalb des Rathauses, öffnet sich die jeweilige Entscheidung.
from flask import Flask, send_from_directory import village_data # das Skript aus Schritt 3 app = Flask(__name__) @app.route("/village/") def village(): return send_from_directory("village", "index.html") @app.route("/village/village_data.json") def data(): village_data.main() # baut bei jedem Aufruf der Seite aus den neuesten Logs neu auf return send_from_directory("village", "village_data.json") if __name__ == "__main__": app.run(port=5007) Enter fullscreen mode Exit fullscreen mode Installiere Flask einmal mit pip install flask, führe das Skript aus, öffne dann localhost:5007/village/ im Browser, und du siehst dein eigenes System.
Die öffentliche Version verwendet dieselbe Seite mit einer anderen Datendatei. Bevor überhaupt etwas meinen Computer verlässt, entfernt eine Funktion alle privaten Inhalte. Anfragen werden zu einem generischen Titel, und Geldbeträge werden gerundet.
def make_public(data): pub = dict(data) pub["requests"] = [{ "id": r["id"], "from": r["from"], "title": "Eine Entscheidung für Gary" } for r in data.get("requests", [])] pub["fields"] = { k: round(v / 50) * 50 for k, v in data["fields"].items()} return pub Enter fullscreen mode Exit fullscreen mode Eine geplante Aufgabe kopiert dann die Seite und die öffentliche Datendatei auf einen neuen Branch meiner Website, öffnet einen Pull Request und führt ihn erst zusammen, nachdem eine automatische Layoutprüfung in einem Headless-Browser und eine SEO-Checkliste beide bestanden wurden. Schlägt eine der Prüfungen fehl, geht nichts live. Warum ich Automationen ihre eigene Arbeit überprüfen lasse, habe ich im Artikel zur unabhängigen Prüfung beschrieben.
Kleiner Pixelfont sieht großartig aus und ist unmöglich zu lesen. Verwende eine große, klare Schrift für Beschriftungen (ich nutze Atkinson Hyperlegible, das für Sehbehinderte entwickelt wurde), starke Kontraste, und blende die kleineren Beschriftungen aus, wenn jemand herauszoomt, damit sie sich nie übereinanderstapeln. Teste es auf einem Smartphone, bevor du es teilst.
Drei Dinge haben mich hereingelegt, die du dir damit sparen kannst.
Ja, wenn die öffentliche Version ihre eigene Datendatei bekommt. Zeige die öffentliche Seite niemals auf deine privaten Daten. Baue eine Funktion wie make_public, die Namen, Anfragedetails und exakte Geldbeträge entfernt, und prüfe ihre Ausgabe vor jeder Veröffentlichung.
Ja. Python, Flask, ein Browser-Canvas, GitHub Pages und der Windows Task Scheduler sind alle kostenlos. Wenn du auch einen KI-Assistenten im Dorf haben möchtest, kannst du einen zu Hause ohne Token-Kosten betreiben, was ich im Artikel zum Bau eines eigenen lokalen KI-Assistenten erkläre.
Könntest du, aber eine Game-Engine bringt viel zu lernen mit sich, bevor du irgendetwas siehst. Eine einzelne HTML-Seite mit einem Canvas lädt überall, lässt sich kostenlos hosten und kann mit Hilfe eines KI-Assistenten leicht verändert werden. Fang einfach an und wechsle erst zu einer Engine, wenn du daraus herauswächst.
Du brauchst am ersten Tag nicht elf Schritte. Bringe ein Gebäude und eine gehende Person mit einer echten Logdatei zum Laufen. Sobald du das erste Mal siehst, wie deine eigene Automation über den Bildschirm läuft, wirst du den Rest bauen wollen.
Wenn du dein eigenes Dorf baust, würde ich es gerne sehen. Die vollständige Version von meinem läuft live auf garystow.co.uk/village, und ich schreibe über praktische KI für Marketer auf garystow.co.uk.
Mit Vorlagen kannst du häufige Fragen schnell beantworten oder Snippets zur Wiederverwendung speichern.
Bist du sicher, dass du diesen Kommentar verbergen möchtest? Er wird in deinem Beitrag ausgeblendet, bleibt aber über den Permalink des Kommentars sichtbar.
Für weitere Maßnahmen kannst du diese Person blockieren und/oder Missbrauch melden.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
Posted on Oct 9 • Originally published at garystow.co.uk
Originally published on garystow.co.uk. The free starter code is on GitHub.
Written by Gary Stow with help from AI tools. Every fact is checked and every article is approved by Gary before it goes live.
I run a lot of automations. One team writes and checks articles for this site, another runs three demo trading strategies, and a supervisor sits over both deciding what needs me. For months the only way to see any of it was a dashboard full of tables. It worked for me, but when my wife or my children asked what I was actually building every evening, a table of job names meant nothing to them.
So I turned the whole thing into a village. I built it to help explain my work to my family, and the visual approach made it far easier for them to understand than anything I could say. The style is a cross between RimWorld and South Park: I love playing RimWorld, and I have always been a fan of South Park.
Every page on this site is a house. Every trading strategy is a field that grows or wilts with its results. Every request walks to the Town Hall, and builders are always working on the next house. You can see the live version in the Village.
Open the live Village ↗ How the village works
This guide shows you how to build your own, one step at a time. You do not need a game engine, and everything here is free. If you can run a Python script, you can do this.
There are only three moving parts, and keeping them separate is what makes this manageable.
The drawing never talks to your automations directly. That means a bug in the village can never break the real work, and you can rebuild the picture as often as you like.
The village can only show what your automations record. Give every script one shared function that adds a line to an activity log. One line of JSON per event, called JSON Lines, is the simplest format there is, because you only ever add to the end of the file.
import json from datetime import datetime def log_activity ( who, text, outcome = " ok " ): row = { " ts ": datetime. now (). strftime ( " %Y-%m-%d %H:%M " ), " who ": who, " text ": text, " outcome ": outcome } with open ( " activity_log.jsonl ", " a ", encoding = " utf-8 " ) as f: f. write ( json. dumps ( row ) + " \n " ) log_activity ( " writer ", " Drafted: how to build a village " ) Enter fullscreen mode Exit fullscreen mode Call it at the important moments: a job starting, finishing, failing, or waiting for a human.
Before drawing anything, write down what each building stands for. This is the part that makes the village make sense to someone who has never seen your system. Mine looks like this:
Keep a simple lookup from the names in your log to the building where that work happens.
Save this as village_data.py. It reads the last 48 hours of the log and writes the file the page will draw from. Run it on a schedule, or every time the page asks for it (Step 9 does that).
import json from datetime import datetime, timedelta from pathlib import Path LOG = Path ( " activity_log.jsonl " ) OUT = Path ( " village/village_data.json " ) BUILDING_FOR = { " writer ": " workshop ", " checker ": " hall ", " publisher ": " site ", " trader ": " field " } def recent_events ( hours = 48 ): cutoff = datetime. now () - timedelta ( hours = hours ) events = [] for line in LOG. read_text ( encoding = " utf-8 " ). splitlines (): e = json. loads ( line ) if datetime. strptime ( e [ " ts " ], " %Y-%m-%d %H:%M " ) >= cutoff: events. append ({ " time ": e [ " ts " ][ 11:], " from ": " hall ", # every job starts at the Town Hall " at ": BUILDING_FOR. get ( e [ " who " ], " hall " ), " text ": e [ " text " ]}) return events [ - 30:] def build ( pages, results ): data = { " generated ": datetime. now (). isoformat ( timespec = " minutes " ), " pages ": pages, " events ": recent_events (), " fields ": results } OUT. parent. mkdir ( exist_ok = True ) OUT. write_text ( json. dumps ( data ), encoding = " utf-8 " ) def main (): build ( pages = [{ " title ": " Home page ", " url ": " / ", " views ": 84 }], results = { " strategy_a ": 120, " strategy_b ": - 40 }) if __name__ == " __main__ ": main () Enter fullscreen mode Exit fullscreen mode The page views can come from any analytics export you already have. Start with made-up numbers and swap in real ones once the picture works.
The RimWorld style look comes from isometric tiles: diamonds that are twice as wide as they are tall. One small function turns a grid square into a position on the screen, and everything else is built on top of it.
const TW = 64, TH = 32; // one tile: 64 wide, 32 tall function toScreen ( x, y ) { // grid square -> pixel position return { sx: ( x - y ) * TW / 2, sy: ( x + y ) * TH / 2 }; } function tile ( ctx, x, y, colour ) { const { sx, sy } = toScreen ( x, y ); ctx. beginPath (); ctx. moveTo ( sx, sy ); ctx. lineTo ( sx + TW / 2, sy + TH / 2 ); ctx. lineTo ( sx, sy + TH ); ctx. lineTo ( sx - TW / 2, sy + TH / 2 ); ctx. closePath (); ctx. fillStyle = colour; ctx. fill (); } Enter fullscreen mode Exit fullscreen mode Loop over a grid and call tile for every square. Use one colour for grass, another for roads, and blue for a river between districts.
A building is three shapes: a darker left wall, a lighter right wall and a roof. Thick black outlines give it the cut-out look. Draw from the back of the map to the front, sorted by x + y, so nearer buildings cover the ones behind them.
function poly ( ctx, pts, fill ) { ctx. beginPath (); pts. forEach (([ px, py ], i ) => i? ctx. lineTo ( px, py ): ctx. moveTo ( px, py )); ctx. closePath (); ctx. fillStyle = fill; ctx. fill (); ctx. lineWidth = 3; ctx. strokeStyle = " #000 "; ctx. stroke (); } function shade ( hex, pct ) { // lighten or darken a colour const n = parseInt ( hex. slice ( 1 ), 16 ); const f = c => Math. max ( 0, Math. min ( 255, c + Math. round ( 2.55 * pct ))); return " # " + [ f ( n >> 16 ), f (( n >> 8 ) & 255 ), f ( n & 255 )]. map ( v => v. toString ( 16 ). padStart ( 2, " 0 " )). join ( "" ); } function box ( ctx, x, y, h, colour ) { // h = height in pixels const { sx, sy } = toScreen ( x, y ); const top = sy - h; poly ( ctx, [[ sx - TW / 2, top + TH / 2 ], [ sx, top + TH ], [ sx, sy + TH ], [ sx - TW / 2, sy + TH / 2 ]], shade ( colour, - 25 )); poly ( ctx, [[ sx, top + TH ], [ sx + TW / 2, top + TH / 2 ], [ sx + TW / 2, sy + TH / 2 ], [ sx, sy + TH ]], shade ( colour, - 10 )); poly ( ctx, [[ sx, top ], [ sx + TW / 2, top + TH / 2 ], [ sx, top + TH ], [ sx - TW / 2, top + TH / 2 ]], colour ); } Enter fullscreen mode Exit fullscreen mode Add windows, doors and signs once the boxes look right. Resist the urge to make it pretty too early. Get the layout working with plain boxes first.
This is the part that brings it to life. Every event in the data file sends a little person from one building to another. When the supervisor approves something, someone walks from the Town Hall to the executor. When an article goes live, a builder walks to the housing district.
const walkers = []; function playEvent ( ev ) { // ev.from and ev.at are building names walkers. push ({ from: BUILDINGS [ ev. from ], to: BUILDINGS [ ev. at ], t: 0 }); } function moveWalkers ( dt ) { // dt = milliseconds since last frame for ( const w of walkers ) w. t = Math. min ( 1, w. t + dt / 4000 ); // 4 seconds a trip for ( let i = walkers. length - 1; i >= 0; i -- ) { if ( walkers [ i ]. t >= 1 ) walkers. splice ( i, 1 ); } } function walkerPos ( w ) { return { x: w. from. x + ( w. to. x - w. from. x ) * w. t, y: w. from. y + ( w. to. y - w. from. y ) * w. t }; } Enter fullscreen mode Exit fullscreen mode Play one event every few seconds and loop back to the start when you run out. Draw a coloured path for each team's flow, so you can see at a glance that website work and trading work never cross.
Now connect the picture to real numbers. These two rules do most of the work in mine.
function fieldScale ( net ) { // net = profit or loss in pounds return Math. max ( 0.6, Math. min ( 1.45, 1 + net / 400 )); } function houseFloors ( views ) { return views > 200? 3: views > 50? 2: 1; } Enter fullscreen mode Exit fullscreen mode Clamping matters. Without the limits, one great or terrible week would make a field fill the screen or vanish. Pick limits that keep the village readable.
Save the page as village/index.html, next to the data file, with a <canvas id="village" width="1200" height="750"> in it. Fetch the data once, then redraw about sixty times a second.
const canvas = document. getElementById ( " village " ); const ctx = canvas. getContext ( " 2d " ); let DATA = null, last = performance. now (); function frame ( now ) { const dt = now - last; last = now; ctx. setTransform ( 1, 0, 0, 1, 0, 0 ); ctx. clearRect ( 0, 0, canvas. width, canvas. height ); ctx. setTransform ( 1, 0, 0, 1, canvas. width / 2, 80 ); // grid square 0,0 sits top centre drawGround (); drawBuildings (); moveWalkers ( dt ); drawWalkers (); requestAnimationFrame ( frame ); } fetch ( " village_data.json " ). then ( r => r. json ()). then ( d => { DATA = d; requestAnimationFrame ( frame ); }); Enter fullscreen mode Exit fullscreen mode drawGround, drawBuildings and drawWalkers are your own functions built from the pieces above.
My private village runs from the same small Flask app as my local dashboard, so it can show everything, including the actual requests waiting for me. Clicking a person outside the Town Hall opens the decision.
from flask import Flask, send_from_directory import village_data # the Step 3 script app = Flask ( __name__ ) @app.route ( " /village/ " ) def village (): return send_from_directory ( " village ", " index.html " ) @app.route ( " /village/village_data.json " ) def data (): village_data. main () # rebuild from the latest logs every time the page asks return send_from_directory ( " village ", " village_data.json " ) if __name__ == " __main__ ": app. run ( port = 5007 ) Enter fullscreen mode Exit fullscreen mode Install Flask once with pip install flask, run the script, then open localhost:5007/village/ in your browser and you are watching your own system.
The public version uses the same page with a different data file. Before anything leaves my computer, a function strips out anything private. Requests become a generic title, and money is rounded.
def make_public ( data ): pub = dict ( data ) pub [ " requests " ] = [{ " id ": r [ " id " ], " from ": r [ " from " ], " title ": " A decision for Gary " } for r in data. get ( " requests ", [])] pub [ " fields " ] = { k: round ( v / 50 ) * 50 for k, v in data [ " fields " ]. items ()} return pub Enter fullscreen mode Exit fullscreen mode A scheduled task then copies the page and the public data file into my website on a new branch, opens a pull request, and only merges it after an automatic layout check in a headless browser and an SEO checklist both pass. If either check fails, nothing goes live. I wrote about why I make automation check its own work in the independent audit.
Small pixel text looks great and is impossible to read. Use a large, clear font for labels (I use Atkinson Hyperlegible, which was designed for low vision), strong contrast, and hide the smaller labels when someone zooms out so they never pile on top of each other. Test it on a phone before you share it.
Three things caught me out, so you can skip them.
Yes, if the public version gets its own data file. Never point the public page at your private data. Build a function like make_public that removes names, request details and exact money figures, and check its output before every publish.
Yes. Python, Flask, a browser canvas, GitHub Pages and Windows Task Scheduler are all free. If you want an AI assistant in the village too, you can run one at home with no token bill, which I explain in how to build your own local AI assistant.
You could, but a game engine adds a lot to learn before you see anything. A single HTML page with a canvas loads anywhere, can be hosted for free and is easy for an AI assistant to help you change. Start simple and move to an engine only if you outgrow it.
You do not need eleven steps on day one. Get one building and one walking person working from a real log file. Once you see your own automation move across the screen for the first time, you will want to build the rest.
If you build your own village, I would love to see it. The full version of mine runs live at garystow.co.uk/village, and I write about practical AI for marketers at garystow.co.uk.