commit 1702c3366cbf710dce9141626b3aec32f113199b
Author: Jens Kristoffersson <jenkr@vgregion.se>
AuthorDate: Tue Sep 8 14:06:25 2026 +0200
Commit: Jens Kristoffersson <jenkr@vgregion.se>
CommitDate: Tue Sep 8 14:06:25 2026 +0200
Grafmotorn: uppdatera wiki + README till nod/kant-modellen
- docs/wiki.html: oversikten beskriver {format:graph, nodes, edges, start},
loop/if via handtag, jump_to_label borttaget, "Etiketter & hopp" ->
"Grenar, sammanslagning & loopar".
- README: webui-avsnittet + "Utveckla webb-UI:t" beskriver fri kabeldragning
och migrate.py / graph_engine.py.
---
README.md | 29 ++++++++--------
docs/wiki.html | 103 ++++++++++++++++++++++++++++++++++-----------------------
2 files changed, 78 insertions(+), 54 deletions(-)
diff --git a/README.md b/README.md
index ef62e8a..4e0b17f 100644
--- a/README.md
+++ b/README.md
@@ -62,16 +62,18 @@ UI:t – se "Utveckla webb-UI:t" nedan.
## Webb-UI: node-canvas
-`python run_webui.py` → öppna http://127.0.0.1:8765. Flödet byggs på ett
-node-canvas (pan/zoom/minikarta): varje steg är en nod, kopplingarna visar
-ordningen, `loop`/`if` expanderas inuti sin nod. Dra en nod för att flytta den
-(positionen sparas med flödet), eller använd ↑/↓ på noden. "Autoplacera"
-återställer layouten.
-
-- **Vänsterpalett** – dra en stegtyp till canvasen för att lägga till den; dra
- den ovanpå en loop/if-nods kropp för att lägga den där. Typ-dropdownen i noden
- finns kvar för att byta typ på plats. Fliken **Block** = återanvändbara
- flödesdelar (se nedan).
+`python run_webui.py` → öppna http://127.0.0.1:8765. Flödet är en **graf** på ett
+node-canvas (pan/zoom/minikarta): varje nod är ett steg, och du **drar kablar**
+mellan noderna för att bestämma körordningen. `if`-noder har ett utgångshandtag
+per gren (`case0`, `case1`, … + `else`); `loop`-noder har `each` (per varv) och
+`done` – koppla kroppens sista nod tillbaka till loopens `in`. Dra en nod för att
+flytta den (positionen sparas med flödet). "Autoplacera" lägger ut grafen
+vänster→höger.
+
+- **Vänsterpalett** – dra en stegtyp till canvasen (eller dubbelklicka) för att
+ lägga till en lös nod; koppla sedan ihop den. Typ-dropdownen i noden finns kvar
+ för att byta typ på plats. Fliken **Block** = återanvändbara flödesdelar (se
+ nedan).
- **⚙-menyn** (uppe till höger) – Nytt/Ta bort flöde, Exportera, close-mode,
körningsinställningar (headless/skip_browser/viewport…), **Kör-parametrar /
indata**, språk, **Inspektera fönster**, Wiki.
@@ -438,9 +440,10 @@ npm test # vitest (graf-round-trip)
Stegtyperna kommer från `GET /api/step-schema` (dvs. [`rpa/schema.py`](rpa/schema.py)),
så nya steg behöver bara läggas där + i `flow_engine.py` – UI:t renderar dem
-automatiskt. Flödet lagras oförändrat som `{steps, settings}`; `graph.ts`
-kompilerar den nästlade listan till/från noder+kanter (se kommentaren där om
-framtida äkta-DAG).
+automatiskt. Flödet lagras som en nod/kant-graf
+(`{format:"graph", nodes, edges, start, settings}`); gamla `{steps}`-flöden
+migreras vid inläsning ([`rpa/migrate.py`](rpa/migrate.py)) och körs av
+[`rpa/graph_engine.py`](rpa/graph_engine.py).
## Bygga en egen fristående automation
diff --git a/docs/wiki.html b/docs/wiki.html
index bfa8683..e214090 100644
--- a/docs/wiki.html
+++ b/docs/wiki.html
@@ -173,7 +173,7 @@
<li><a href="#interpolering">Variabler i text ({var})</a></li>
<li><a href="#loop">Loop: DOM vs. lista</a></li>
<li><a href="#recept">Recept (arbetsflöden)</a></li>
- <li><a href="#etiketter">Etiketter & hopp</a></li>
+ <li><a href="#etiketter">Grenar, sammanslagning & loopar</a></li>
<li><a href="#felsokning">Felsökning</a></li>
<li><a href="#datamappar">Datamappar</a></li>
<li><a href="#sprak">Språk (i18n)</a></li>
@@ -194,17 +194,29 @@
<section id="oversikt">
<h2>Översikt</h2>
<p>
- Ett flöde är en JSON-fil (<code>data/flows/<namn>.json</code>) med en
- lista av <strong>steg</strong>. Varje steg har en <code>type</code> och
- typ-specifika fält. Webb-UI:t (<code>python run_webui.py</code>,
- <a href="http://127.0.0.1:8765">http://127.0.0.1:8765</a>) bygger och
- redigerar dessa JSON-filer visuellt, men de går lika bra att redigera för
- hand.
+ Ett flöde är en JSON-fil (<code>data/flows/<namn>.json</code>) i
+ <strong>grafform</strong>: <code>{"format":"graph", "nodes":[…], "edges":[…],
+ "start":"<nod-id>"}</code>. Varje <strong>nod</strong> har ett
+ <code>id</code>, en <code>type</code> och typ-specifika fält (platt – ingen
+ nästling). <strong>Kanterna</strong> bestämmer körordningen. Webb-UI:t
+ (<code>python run_webui.py</code>,
+ <a href="http://127.0.0.1:8765">http://127.0.0.1:8765</a>) bygger grafen
+ visuellt – dra kablar mellan noderna.
</p>
<p>
- Motorn som kör flödet är <code>rpa/flow_engine.py</code> –
- <code>run_flow()</code> kör en lista av steg sekventiellt, med stöd för
- loopar, villkor och hopp (se nedan).
+ Gamla flöden i det nästlade <code>{"steps":[…]}</code>-formatet läses
+ fortfarande; de migreras automatiskt till grafform vid inläsning och skrivs
+ om vid nästa sparning.
+ </p>
+ <p>
+ Motorn är <code>rpa/graph_engine.py</code> – <code>run_graph()</code> börjar
+ på startnoden och följer kanterna. Efter en vanlig nod följs handtaget
+ <code>out</code>. <code>if</code>-noder har ett handtag per gren
+ (<code>case0</code>, <code>case1</code>, … + <code>else</code>);
+ <code>loop</code>-noder har <code>each</code> (en gång per varv) och
+ <code>done</code> (efteråt), och kroppens sista nod kopplas tillbaka till
+ loop-nodens <code>in</code>. <code>rpa/flow_engine.py</code> är kvar som
+ omslag och kör de enskilda stegen.
</p>
</section>
@@ -529,41 +541,44 @@ run_script scripts/merge_and_print.py
<li><code>over</code> – variabelnamn att loopa över</li>
<li><code>item_var?</code> – variabelnamn för aktuellt värde vid loop över en lista (default <code>item</code>)</li>
<li><code>index_var?</code> – variabelnamn för varvräknaren <code>1, 2, 3 …</code> (default <code>index</code>); sätts oavsett looptyp och kan användas som <code>{index}</code> i t.ex. filnamn</li>
- <li><code>steps</code> – nästlade steg som körs en gång per post</li>
</ul>
- <p>Se <a href="#loop">Loop: DOM vs. lista</a> för de två lägena.</p>
+ <p>Utgångar: <code>each</code> (körs en gång per post, sätter
+ <code>item</code>/<code>index</code>) och <code>done</code> (efter sista
+ varvet). <strong>Kroppens sista nod måste ha en kant tillbaka till
+ loop-nodens <code>in</code></strong> – annars körs varv 0 en gång och
+ <code>done</code> nås aldrig. Se <a href="#loop">Loop: DOM vs. lista</a>
+ för de två <code>over</code>-lägena.</p>
</div>
<div class="step-card">
<h3><code>if</code> <span class="tag">Om / annars om / annars</span></h3>
<ul class="field-list">
- <li><code>branches</code> – lista av <code>{condition, steps}</code>, i ordning (första sanna vinner)</li>
- <li><code>else?</code> – körs om ingen gren matchade</li>
+ <li><code>branches</code> – lista av <code>{condition}</code>, i ordning (första sanna vinner)</li>
</ul>
- <p>Se <a href="#villkor">Villkor</a>.</p>
- </div>
-
- <div class="step-card">
- <h3><code>jump_to_label</code> <span class="tag">Hoppa till etikett</span></h3>
- <ul class="field-list"><li><code>target</code> – etikettnamnet att hoppa till</li></ul>
- <p>Se <a href="#etiketter">Etiketter & hopp</a>.</p>
+ <p>Utgångar: <code>case0</code>, <code>case1</code>, … (en per gren) samt
+ <code>else</code> (ingen gren matchade). Koppla varje grens noder till
+ respektive utgång. Matchar ingen gren och det saknas en
+ <code>else</code>-kant tar den vägen slut. Se <a href="#villkor">Villkor</a>.</p>
</div>
<div class="step-card">
- <h3>Universella fält (alla steg)</h3>
+ <h3>Universella fält (alla noder)</h3>
<ul class="field-list">
- <li><code>enabled</code> – <code>false</code> hoppar över steget (visas som en kryssruta i UI:t). En avstängd <code>loop</code>/<code>if</code> hoppar över hela blocket.</li>
- <li><code>label</code> – valfritt namn på steget, används som mål för <code>jump_to_label</code>.</li>
- <li><code>on_error</code> – <code>"stop"</code> (default) avbryter körningen om steget misslyckas/får timeout; <code>"continue"</code> loggar felet och går vidare (steget blir "hit or miss"). Väljs i listan bredvid etikettfältet. På en <code>loop</code>/<code>if</code> gäller det hela blocket.</li>
+ <li><code>enabled</code> – <code>false</code> hoppar över noden (kryssruta i UI:t); körningen fortsätter via <code>out</code>.</li>
+ <li><code>label</code> – valfritt namn på noden (visas i körloggen).</li>
+ <li><code>on_error</code> – <code>"stop"</code> (default) avbryter körningen om noden misslyckas/får timeout; <code>"continue"</code> loggar felet och går vidare. Väljs i listan bredvid etikettfältet.</li>
</ul>
+ <p class="hint"><code>jump_to_label</code> finns inte längre – rita en kant
+ i stället.</p>
</div>
</section>
<section id="villkor">
<h2>Villkor (If)</h2>
- <p><code>if</code>-steget utvärderar <code>branches</code> i ordning – första
- grenen vars <code>condition</code> är sann kör sina <code>steps</code> och
- resten hoppas över. Ingen gren sann → <code>else</code> körs om den finns,
+ <p><code>if</code>-noden utvärderar <code>branches</code> i ordning – första
+ grenen vars <code>condition</code> är sann följer sin <code>case</code>-utgång
+ och resten hoppas över. Ingen gren sann → <code>else</code>-utgången följs om
+ den är kopplad,
annars fortsätter flödet till nästa steg.</p>
<table>
<tr><th>kind</th><th>Fält</th><th>Betyder</th></tr>
@@ -580,6 +595,7 @@ run_script scripts/merge_and_print.py
<code>"Tilldelat"</code> från en som innehåller <code>"Automatiskt tilldelat"</code>.
</div>
<pre><code>{
+ "id": "kollaTilldelad",
"type": "if",
"branches": [
{
@@ -587,11 +603,12 @@ run_script scripts/merge_and_print.py
"kind": "element_not_exists",
"by": "css", "scope": "item",
"value": ":text-is(\"Tilldelat\")"
- },
- "steps": [ /* körs bara för rader som INTE redan är Tilldelat */ ]
+ }
}
]
-}</code></pre>
+}
+// kant kollaTilldelad --case0--> (noderna som bara ska köras för rader som INTE
+// redan är Tilldelat), och deras sista nod --out--> tillbaka till loopens in.</code></pre>
</section>
<section id="interpolering">
@@ -732,16 +749,20 @@ run_script path "scripts/merge_and_print.py"
</section>
<section id="etiketter">
- <h2>Etiketter & hopp</h2>
- <p>Vilket steg som helst kan få ett <code>label</code> (syns som ett textfält
- bredvid typ-väljaren i UI:t). Ett <code>jump_to_label</code>-steg med
- matchande <code>target</code> hoppar dit, framåt eller bakåt.</p>
+ <h2>Grenar, sammanslagning & loopar</h2>
+ <p>I grafen finns inga hopp – du ritar kanter. Vanliga mönster:</p>
<ul>
- <li>Fungerar bara mellan <strong>syskon</strong> på samma nivå – ett hopp
- kan inte gå in i eller ut ur en <code>loop</code>/<code>if</code>-gren.</li>
- <li>Ett skydd stoppar körningen efter max 1000 hopp i en och samma körning
- (troligen en oändlig hopp-loop) – <code>FlowError</code> istället för att
- hänga sig.</li>
+ <li><strong>Gren + sammanslagning:</strong> koppla <code>if</code>-nodens
+ <code>case</code>-/<code>else</code>-utgångar till varsin nodkedja, och låt
+ varje kedjas sista nod peka på samma efterföljande nod.</li>
+ <li><strong>Loop:</strong> <code>loop.each</code> → kroppens första nod;
+ kroppens sista nod → <code>loop.in</code> (loopback); <code>loop.done</code>
+ → det som ska hända efteråt.</li>
+ <li>Ett utgångshandtag styr bara <strong>en</strong> kant (motorn följer den
+ första). Vill du köra flera saker: kedja dem, eller lägg in en nod som
+ samlar ihop.</li>
+ <li>Ett skydd stoppar körningen efter väldigt många nodsteg (troligen en
+ loop utan avslut) – <code>FlowError</code> istället för att hänga sig.</li>
</ul>
</section>
@@ -786,7 +807,7 @@ run_script path "scripts/merge_and_print.py"
<h2>Inställningar & export</h2>
<p>"⚙ Settings" i headern öppnar en dialog med körningsinställningar,
sparade som ett <code>settings</code>-objekt tillsammans med flödets
- <code>steps</code> i samma JSON-fil (<code>data/flows/<namn>.json</code>):</p>
+ <code>nodes</code>/<code>edges</code> i samma JSON-fil (<code>data/flows/<namn>.json</code>):</p>
<table>
<tr><th>Nyckel</th><th>Standard</th><th>Betyder</th></tr>
<tr><td><code>headless</code></td><td><code>false</code></td><td>Kör utan synligt fönster. Kräver en redan giltig inloggningssession.</td></tr>