foxygit / RPA-Remisser Log in
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 &amp; hopp</a></li>
+    <li><a href="#etiketter">Grenar, sammanslagning &amp; 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/&lt;namn&gt;.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/&lt;namn&gt;.json</code>) i
+      <strong>grafform</strong>: <code>{"format":"graph", "nodes":[…], "edges":[…],
+      "start":"&lt;nod-id&gt;"}</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 &amp; 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 &amp; 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 &amp; 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 &amp; 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/&lt;namn&gt;.json</code>):</p>
+      <code>nodes</code>/<code>edges</code> i samma JSON-fil (<code>data/flows/&lt;namn&gt;.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>