diff --git a/docs/superpowers/plans/2026-09-29-flow-field-list-edit-panel.md b/docs/superpowers/plans/2026-09-29-flow-field-list-edit-panel.md
new file mode 100644
index 0000000..ce950e2
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-29-flow-field-list-edit-panel.md
@@ -0,0 +1,690 @@
+# Flows/Fields: farvet liste + redigeringsvindue — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
+
+**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
+
+**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
+
+**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
+
+## Global Constraints
+
+- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
+- Rækker er ≈835 px brede (som lagervisningen).
+- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
+- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
+- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
+- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
+
+## Review Focus
+
+- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
+- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
+- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
+- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
+- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
+
+---
+
+### Task 1: Rene statusfunktioner for flows (+ fælles farver)
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
+- Test: `tests/test_flowStatus.lua` (ny)
+
+**Interfaces:**
+- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
+ `ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
+
+- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
+
+```lua
+-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adFlowView.lua")
+local V = ADFlowView
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then print("OK " .. name) else
+ failures = failures + 1
+ print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
+ end
+end
+check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
+check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
+check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
+check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
+check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
+check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
+check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
+check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
+check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)
+```
+
+- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
+
+- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
+
+```lua
+-- ---- status pr. flow (Flows-listen) ----
+
+ADFlowView.LEVEL_COLORS = {
+ running = {0.30, 0.75, 0.35, 1},
+ waiting = {0.95, 0.80, 0.25, 1},
+ error = {0.90, 0.35, 0.30, 1},
+ stopped = {0.55, 0.55, 0.55, 1},
+}
+
+-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
+function ADFlowView.flowStatus(s)
+ if s.stopped then return "stopped" end
+ if s.warning ~= nil then return "error" end
+ if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
+ if (s.running or 0) > 0 and s.reason == nil then return "running" end
+ if (s.running or 0) > 0 or s.canStop then return "waiting" end
+ return "stopped"
+end
+```
+
+- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
+- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
+
+---
+
+### Task 2: Rækker kan tegnes i et andet layout
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
+
+**Interfaces:**
+- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
+
+- [ ] **Step 1: `addRow`** — replace body:
+
+```lua
+function SmartPickupFrame:addRow(prefab, data)
+ local layout = self.rowTarget or self.boxLayout
+ local row = prefab:clone(layout)
+ row.spuLayout = layout
+ FocusManager:loadElementFromCustomValues(row)
+ table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
+ local option = row:getDescendantByName("option")
+ if option ~= nil then
+ option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
+ self.rowData[option] = data
+ end
+ return row
+end
+
+-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
+function SmartPickupFrame:withRowTarget(layout, fn)
+ local previous = self.rowTarget
+ self.rowTarget = layout
+ local ok, err = pcall(fn, self)
+ self.rowTarget = previous
+ if not ok then error(err, 0) end
+end
+
+function SmartPickupFrame:clearPanelRows()
+ for _, row in ipairs(self.panelRows or {}) do
+ FocusManager:removeElement(row)
+ row:delete()
+ end
+ self.panelRows = {}
+end
+```
+
+- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
+- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
+
+```lua
+function SmartPickupFrame:colorLayout(layout)
+ local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
+ if colors == nil or layout == nil then return end
+ local unpackFn = table.unpack or unpack
+ local isEvenRow = false
+ for _, element in ipairs(layout.elements) do
+ if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
+ element:setImageColor(nil, unpackFn(colors[isEvenRow]))
+ end
+ isEvenRow = not isEvenRow
+ end
+end
+
+function SmartPickupFrame:colorRows()
+ self:colorLayout(self.boxLayout)
+end
+```
+
+- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
+
+```lua
+ local layout = element.spuLayout or frame.boxLayout
+ local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
+ if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
+ and isInside(element, posX, posY) and isInside(layout, posX, posY) then
+```
+(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
+
+- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
+- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
+
+---
+
+### Task 3: Redigeringsvinduet (overlay + mixin)
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
+- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
+- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
+- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
+- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
+
+**Interfaces:**
+- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
+- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
+
+- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before ``):
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: XML overlay** — directly after `` of `boxLayout`:
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
+
+```lua
+-- Smart Pickup — redigeringsvindue (v1.58)
+-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
+-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
+-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
+-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
+
+SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
+SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
+
+function SmartPickupFrame:initEditPanelButtons(i18n)
+ self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
+ self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
+ self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
+ self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
+end
+
+function SmartPickupFrame:editPanelKind()
+ return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
+end
+
+-- Vinduet dækker listens flade; panelet er centreret i den.
+function SmartPickupFrame:layoutEditPanel()
+ local box = self.boxLayout
+ local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
+ self.editBackdrop:setPosition(x, y)
+ self.editBackdrop:setSize(w, h)
+ local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
+ self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
+ self.editPanel:setSize(pw, ph)
+ local top = 80 * g_pixelSizeScaledY
+ self.editLayout:setSize(pw, ph - top)
+end
+
+function SmartPickupFrame:openEditPanel(kind, id)
+ if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
+ self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
+ self:closeGuide()
+ self.editOverlay:setVisible(true)
+ self:layoutEditPanel()
+ self:requestRebuild()
+end
+
+function SmartPickupFrame:closeEditPanel()
+ if not self.editPanelOpen then return end
+ self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
+ self:closeGuide()
+ self:clearPanelRows()
+ if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
+ self:requestRebuild()
+end
+
+-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
+function SmartPickupFrame:fillEditPanel(adEnv)
+ self:clearPanelRows()
+ local hooks = self:editPanelKind()
+ if hooks == nil then return end
+ if not hooks.exists(self, self.editPanelId) then
+ self.statusOverride = self.i18n:getText("spu_panel_gone")
+ self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
+ self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
+ self.editOverlay:setVisible(false)
+ return
+ end
+ self.editTitle:setText(hooks.title(self, self.editPanelId))
+ self:withRowTarget(self.editLayout, function(frame)
+ if frame.guide ~= nil then
+ frame:fillGuideRows(adEnv)
+ else
+ hooks.fill(frame, adEnv, frame.editPanelId)
+ end
+ end)
+ self:colorLayout(self.editLayout)
+ self.editLayout:invalidateLayout()
+ self:refreshEditPanel(adEnv)
+end
+
+-- Hvert sekund: status + knaptekster.
+function SmartPickupFrame:refreshEditPanel(adEnv)
+ local hooks = self:editPanelKind()
+ if hooks == nil or adEnv == nil then return end
+ if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
+ local status = hooks.status(self, adEnv, self.editPanelId)
+ self.editStatus:setText(status.text or "")
+ local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
+ if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
+ self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
+ self.panelDeleteButtonInfo.disabled = not hooks.canDelete
+end
+
+function SmartPickupFrame:onClickPanelClose()
+ self:safe(function(frame)
+ -- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
+ if frame.guide ~= nil then
+ frame:closeGuide()
+ frame:requestRebuild()
+ return
+ end
+ frame:closeEditPanel()
+ end)
+end
+
+function SmartPickupFrame:onClickPanelStartStop()
+ self:safe(function(frame)
+ local hooks = frame:editPanelKind()
+ if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
+ frame:requestRebuild()
+ end)
+end
+
+function SmartPickupFrame:onClickPanelDelete()
+ self:safe(function(frame)
+ local hooks = frame:editPanelKind()
+ if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
+ end)
+end
+```
+
+- [ ] **Step 4: Wire into frame** —
+ - `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
+ - `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
+ - `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
+ - `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
+ - `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
+ - `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
+ - `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
+ - Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
+- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
+- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
+- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
+
+---
+
+### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
+- Modify: `l10n/*.xml`
+
+**Interfaces:**
+- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
+- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
+
+- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
+
+```lua
+-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
+function SmartPickupFrame:toggleRun(adEnv, runId)
+ local run = ADRuns.getRun(ADRunsController.store, runId)
+ if adEnv == nil or run == nil then return end
+ self.lastStartFailed = self.lastStartFailed or {}
+ if ADRunsController.getRunStatus(adEnv, run).canStop then
+ ADRunsController.stopRun(adEnv, run.id)
+ self.lastStartFailed[run.id] = nil
+ self.statusOverride = nil
+ else
+ local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
+ self.lastStartFailed[run.id] = started == 0 and skipped > 0
+ local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
+ self:showOverride(text, reasons)
+ if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
+ ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
+ end
+ end
+ ADRunsController.save()
+end
+
+function SmartPickupFrame:flowLevel(adEnv, run)
+ local row = ADRunsController.getFlowRow(adEnv, run)
+ local status = ADRunsController.getRunStatus(adEnv, run)
+ return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
+ canStop = status.canStop == true, warning = row.warning, reason = row.reason,
+ lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
+end
+```
+ And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
+
+- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
+
+```lua
+function SmartPickupFrame:fillFlowTabRows(adEnv)
+ if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
+ self.flowListRows = {}
+ local i18n = self.i18n
+ if #ADRunsController.store.runs == 0 then
+ self:addTextRow(i18n:getText("spu_noRuns"))
+ return
+ end
+ self:addTextRow(i18n:getText("spu_flowList_hint"))
+ for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
+ self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
+ for _, run in ipairs(group.runs) do
+ local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
+ local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
+ cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
+ row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
+ local runId = run.id
+ local actionCell = entry.cells[5]
+ self:makeRowClickable(row, function(frame)
+ -- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
+ if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
+ frame:toggleRun(frame.getAdEnv(), runId)
+ frame:requestRebuild()
+ else
+ frame.selectedRunId = runId
+ frame:openEditPanel("flow", runId)
+ end
+ end)
+ table.insert(self.flowListRows, entry)
+ end
+ end
+ self:refreshFlowListRows(adEnv)
+end
+
+function SmartPickupFrame:refreshFlowListRows(adEnv)
+ local unpackFn = table.unpack or unpack
+ for _, entry in ipairs(self.flowListRows or {}) do
+ local run = ADRuns.getRun(ADRunsController.store, entry.runId)
+ if run ~= nil then
+ local cells = self:flowCells(adEnv, run)
+ local level = self:flowLevel(adEnv, run)
+ local color = ADFlowView.LEVEL_COLORS[level]
+ local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
+ entry.cells[1]:setText(tostring(run.name))
+ entry.cells[2]:setText(tostring(cells[2]))
+ entry.cells[3]:setText(tostring(cells[5]))
+ entry.cells[4]:setText(tostring(cells[6]))
+ entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
+ if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
+ if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
+ end
+ end
+end
+```
+ In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
+
+- [ ] **Step 4: `refreshFlowTab`** — replace body:
+
+```lua
+function SmartPickupFrame:refreshFlowTab(adEnv)
+ if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
+ self:setStatusText(self.statusOverride or "")
+ return
+ end
+ self:refreshFlowListRows(adEnv)
+ local running, total = 0, 0
+ for _, run in ipairs(ADRunsController.store.runs) do
+ total = total + 1
+ if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
+ end
+ self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
+end
+```
+
+- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
+
+```lua
+SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
+ canDelete = true,
+ exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
+ title = function(frame, id)
+ local run = ADRuns.getRun(ADRunsController.store, id)
+ return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
+ end,
+ fill = function(frame, adEnv, id)
+ local run = ADRuns.getRun(ADRunsController.store, id)
+ if run ~= nil then frame:fillFlowSummary(adEnv, run) end
+ end,
+ status = function(frame, adEnv, id)
+ local run = ADRuns.getRun(ADRunsController.store, id)
+ return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
+ end,
+ startStopText = function(frame, adEnv, id)
+ local run = ADRuns.getRun(ADRunsController.store, id)
+ return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
+ end,
+ startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
+ delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
+}
+```
+ In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
+ After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
+
+- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
+- [ ] **Step 7: Run all tests + XML check.**
+- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
+
+---
+
+### Task 5: Etape 1 build + in-game test
+
+- [ ] **Step 1:** bump `modDesc.xml` `` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '[^<]*'` shows 1.58.0.0.
+- [ ] **Step 2:** commit + push gitea main.
+- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
+
+---
+
+### Task 6: Ren statusfunktion for marker
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/adFieldView.lua`
+- Test: `tests/test_fieldStatus.lua` (ny)
+
+**Interfaces:**
+- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
+
+- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
+
+```lua
+-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adFieldView.lua")
+local V = ADFieldView
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then print("OK " .. name) else
+ failures = failures + 1
+ print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
+ end
+end
+check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
+check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
+check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
+check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
+check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
+check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
+check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
+check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
+check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)
+```
+ (If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
+- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
+- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
+
+```lua
+-- ---- status pr. mark (Fields-listen) ----
+
+-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
+ADFieldView.ERROR_REASONS = {
+ spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
+ spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
+}
+
+-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
+function ADFieldView.fieldStatus(s)
+ if not s.hasMarker then return "error" end
+ if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
+ if s.awaiting or s.startRequested then return "waiting" end
+ if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
+ if s.reasonKey ~= nil then return "waiting" end
+ return "stopped"
+end
+```
+- [ ] **Step 4: Run all tests** — green.
+- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
+
+---
+
+### Task 7: Fields-fanen som farvet liste + "field" i vinduet
+
+**Files:**
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
+- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
+- Modify: `l10n/*.xml`
+
+**Interfaces:**
+- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
+- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
+
+- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+ The header row (column titles) passes a 7th title `""`.
+- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
+
+```lua
+function SmartPickupFrame:fieldLevel(id)
+ local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
+ if field == nil or info == nil then return "stopped" end
+ local reason = type(field.reason) == "table" and field.reason.key or nil
+ return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
+ awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
+ reasonKey = reason})
+end
+
+function SmartPickupFrame:fieldActionText(id)
+ local field = ADFieldJobs.getField(id)
+ if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
+ return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
+end
+```
+ (If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
+- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
+
+```lua
+ self:makeRowClickable(cells[1].parent, function(frame)
+ if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
+ frame:toggleField(fieldId)
+ else
+ frame.selectedFieldId = fieldId
+ frame:openEditPanel("field", fieldId)
+ end
+ end)
+```
+ Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
+- [ ] **Step 4: Panel kind** —
+
+```lua
+SmartPickupFrame.EDIT_PANEL_KINDS.field = {
+ canDelete = false,
+ exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
+ title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
+ fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
+ status = function(frame, adEnv, id)
+ return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
+ end,
+ startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
+ startStop = function(frame, id) frame:toggleField(id) end,
+ delete = function() end,
+}
+```
+ `fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
+ `getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
+- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
+- [ ] **Step 6: Run all tests + XML check.**
+- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
+
+---
+
+### Task 8: Etape 2 build + in-game test
+
+- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
+- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
+- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.