diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index d0a1b93..30e40b3 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -20,7 +20,7 @@ Modularize the workflow for maintainability, add "update all" functionality, fix
**Requirements:** MOD-01, MOD-02, DEBT-03
-**Plans:** 6 plans
+**Plans:** 7 plans
Plans:
- [x] 10-01-PLAN.md — Orphan node cleanup (removed 2 orphan nodes)
@@ -29,6 +29,7 @@ Plans:
- [x] 10-04-PLAN.md — Integration verification and user checkpoint
- [x] 10-05-PLAN.md — Complete modularization (batch operations, logs sub-workflow)
- [x] 10-06-PLAN.md — Remediation: fix routing gaps, wire logs, cleanup Python scripts
+- [ ] 10-07-PLAN.md — UAT gap closure: race conditions, data chains, fuzzy matching, error handling
**Success Criteria:**
1. ✓ Workflow split into logical sub-workflows (update, actions, logs)
@@ -154,7 +155,7 @@ Plans:
| 7 | Socket Security | v1.1 | Complete |
| 8 | Inline Keyboard Infrastructure | v1.1 | Complete |
| 9 | Batch Operations | v1.1 | Complete |
-| 10 | Workflow Modularization | v1.2 | Complete |
+| 10 | Workflow Modularization | v1.2 | UAT Gaps |
| 10.1 | Aggressive Workflow Modularization | v1.2 | Pending (INSERTED) |
| 10.2 | Better Logging & Log Management | v1.2 | Pending (INSERTED) |
| 11 | Update All & Callback Limits | v1.2 | Pending |
@@ -164,4 +165,4 @@ Plans:
**v1.2 Coverage:** 12+ requirements mapped across 7 phases
---
-*Updated: 2026-02-04 after Phase 10 planning*
+*Updated: 2026-02-04 after Phase 10 UAT gap planning*
diff --git a/.planning/phases/10-workflow-modularization/10-07-PLAN.md b/.planning/phases/10-workflow-modularization/10-07-PLAN.md
new file mode 100644
index 0000000..9147e48
--- /dev/null
+++ b/.planning/phases/10-workflow-modularization/10-07-PLAN.md
@@ -0,0 +1,345 @@
+---
+phase: 10-workflow-modularization
+plan: 07
+type: remediation
+wave: 6
+depends_on: [10-06]
+files_modified: [n8n-workflow.json]
+autonomous: true
+gap_closure: true
+
+must_haves:
+ truths:
+ - "Single text update sends only one result message (no race condition)"
+ - "Batch update executes via Container Update sub-workflow"
+ - "Batch actions execute via Container Actions sub-workflow"
+ - "Container logs text command works with fuzzy matching"
+ - "Refresh Logs button handles 'message not modified' gracefully"
+ artifacts:
+ - path: "n8n-workflow.json"
+ provides: "Fixed main workflow with all UAT gaps closed"
+ contains: "$('Build Progress Message').item.json"
+ key_links:
+ - from: "Prepare Text Update Input"
+ to: "Execute Text Update"
+ via: "Sequential connection (not parallel with Send Text Update Started)"
+ - from: "Prepare Batch Update Input"
+ to: "Execute Batch Update"
+ via: "Uses $('Build Progress Message').item.json.container"
+ - from: "Prepare Batch Action Input"
+ to: "Execute Batch Action Sub-workflow"
+ via: "Uses $('Build Progress Message').item.json"
+---
+
+
+Close 5 UAT gaps discovered during user testing of Phase 10 modularization.
+
+Purpose: UAT revealed race conditions, broken data chains, missing fuzzy matching, and unhandled Telegram API errors that prevent the modularized workflow from functioning correctly.
+
+Output: Fully functional modularized workflow with all user-reported issues fixed.
+
+
+
+@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
+@/home/luc/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/10-workflow-modularization/10-06-SUMMARY.md
+@n8n-workflow.json
+@n8n-container-logs.json
+
+
+
+
+
+ Task 1: Fix text update race condition (remove parallel message)
+ n8n-workflow.json
+
+**Problem:** User reported two messages sent for single text update - "already up to date" AND "Updating..." in race condition. Root cause: `Prepare Text Update Input` connects in PARALLEL to both `Send Text Update Started` AND `Execute Text Update`. The sub-workflow also sends a result message.
+
+**Current connection (lines 6616-6629):**
+```json
+"Prepare Text Update Input": {
+ "main": [
+ [
+ { "node": "Send Text Update Started", ... },
+ { "node": "Execute Text Update", ... }
+ ]
+ ]
+}
+```
+Both nodes are in the same array = parallel execution.
+
+**Fix:** Remove `Send Text Update Started` from the connection array. The sub-workflow handles all messaging, so the "Updating..." message is redundant.
+
+**Steps:**
+1. Find the "Prepare Text Update Input" entry in the `connections` object
+2. Remove the object `{ "node": "Send Text Update Started", "type": "main", "index": 0 }` from the array
+3. Keep only `{ "node": "Execute Text Update", "type": "main", "index": 0 }`
+
+**Result connection:**
+```json
+"Prepare Text Update Input": {
+ "main": [
+ [
+ { "node": "Execute Text Update", "type": "main", "index": 0 }
+ ]
+ ]
+}
+```
+
+
+- Grep for "Prepare Text Update Input" in connections shows only Execute Text Update
+- No reference to Send Text Update Started in parallel
+
+ Text update sends only one message (from sub-workflow)
+
+
+
+ Task 2: Fix batch update data chain (use Build Progress Message reference)
+ n8n-workflow.json
+
+**Problem:** User reported "Cannot read properties of undefined (reading 'id')" in Prepare Batch Update Input. Root cause: The node uses `$json.container` but receives Telegram API response from `Edit Progress Message`, not the batch data.
+
+**Current code (node id: caeae5d6-f9ec-4aa3-83d3-198b6b55be65, lines 4688-4699):**
+```javascript
+const data = $json;
+const container = data.container;
+```
+
+**Data flow:**
+- `Build Progress Message` outputs: `{ container: {...}, chatId, progressMessageId, action, ... }`
+- `Edit Progress Message` sends Telegram API call, outputs Telegram response (no container data)
+- `Prepare Batch Update Input` receives Telegram response (wrong data!)
+
+**Fix:** Change to reference `Build Progress Message` directly instead of relying on `$json`:
+
+```javascript
+// Prepare input for Container Update sub-workflow
+const data = $('Build Progress Message').item.json;
+const container = data.container;
+
+// Extract container info
+const containerId = container.id || container.Id || '';
+const containerName = container.name || container.Name || '';
+
+return {
+ json: {
+ containerId: containerId,
+ containerName: containerName,
+ chatId: data.chatId,
+ messageId: data.progressMessageId || 0,
+ responseMode: "inline"
+ }
+};
+```
+
+**Steps:**
+1. Find node with id "caeae5d6-f9ec-4aa3-83d3-198b6b55be65" (Prepare Batch Update Input)
+2. Update the `jsCode` parameter to use `$('Build Progress Message').item.json` instead of `$json`
+3. Keep all other logic the same
+
+
+- Grep for "Prepare Batch Update Input" shows `$('Build Progress Message').item.json`
+- No `const data = $json` in that node
+
+ Batch update correctly reads container data from Build Progress Message
+
+
+
+ Task 3: Fix batch action data chain (use Build Progress Message reference)
+ n8n-workflow.json
+
+**Problem:** Same as Task 2 - "Cannot read properties of undefined (reading 'id')" in Prepare Batch Action Input. Uses `$json.container` but receives Telegram response.
+
+**Current code (node id: 958f19ef-249b-42ca-8a29-ecb91548f1dd, lines 4732-4743):**
+```javascript
+const data = $json;
+const container = data.container;
+const action = data.action;
+```
+
+**Fix:** Change to reference `Build Progress Message` directly:
+
+```javascript
+// Prepare input for Container Actions sub-workflow
+const data = $('Build Progress Message').item.json;
+const container = data.container;
+const action = data.action;
+
+// Extract container info
+const containerId = container.id || container.Id || '';
+const containerName = container.name || container.Name || '';
+
+return {
+ json: {
+ containerId: containerId,
+ containerName: containerName,
+ action: action,
+ chatId: data.chatId,
+ messageId: data.progressMessageId || 0,
+ responseMode: "inline"
+ }
+};
+```
+
+**Steps:**
+1. Find node with id "958f19ef-249b-42ca-8a29-ecb91548f1dd" (Prepare Batch Action Input)
+2. Update the `jsCode` parameter to use `$('Build Progress Message').item.json` instead of `$json`
+3. Keep all other logic the same
+
+
+- Grep for "Prepare Batch Action Input" shows `$('Build Progress Message').item.json`
+- No `const data = $json` in that node
+
+ Batch action correctly reads container data from Build Progress Message
+
+
+
+ Task 4: Add fuzzy matching to logs text command and fix Send Logs Response chatId
+ n8n-workflow.json
+
+**Problem:** Two issues with logs text command:
+1. Fuzzy matching not working - exact name required (unlike update/start/stop)
+2. Send Logs Response uses `$json.chatId` but sub-workflow returns `{success, message, containerName}` without chatId
+
+**Current flow:**
+```
+Parse Logs Command -> Prepare Text Logs Input -> Execute Text Logs -> Send Logs Response
+```
+
+**Issue 1 - No fuzzy matching:** The logs flow passes `containerQuery` directly to sub-workflow which does exact match only. Other commands (update/start/stop) have Docker query + fuzzy matching nodes BEFORE calling sub-workflow.
+
+**Issue 2 - Missing chatId:** Send Logs Response (node id: telegram-send-logs) uses:
+```json
+"chatId": "={{ $json.chatId }}"
+```
+But `$json` is the sub-workflow output which doesn't include chatId.
+
+**Fix for Send Logs Response:** Change chatId to reference the input node that HAS chatId:
+```json
+"chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}"
+```
+
+**Note on fuzzy matching:** Adding full fuzzy matching infrastructure (Docker query + Match Container node + Check Match Count switch) would require significant changes. For now, the sub-workflow already does name lookup via Docker API. The issue is it requires EXACT normalized name.
+
+**Alternative simpler fix:** Update the logs sub-workflow's "Find Container" node to use `.includes()` instead of exact `===` match. This provides fuzzy matching without restructuring main workflow.
+
+**Steps:**
+1. In n8n-workflow.json, find node id "telegram-send-logs" (Send Logs Response)
+2. Change `chatId` from `={{ $json.chatId }}` to `={{ $('Prepare Text Logs Input').item.json.chatId }}`
+
+3. In n8n-container-logs.json, find "Find Container" node (id: 52dd705b-dd3b-4fdc-8484-276845857ad0)
+4. Change the filter logic from exact match to includes:
+ - FROM: `normalizeName(c.Names[0]) === containerName`
+ - TO: `normalizeName(c.Names[0]).includes(containerName)`
+5. Add handling for multiple matches (throw error suggesting which containers match)
+
+
+- Send Logs Response uses `$('Prepare Text Logs Input').item.json.chatId`
+- n8n-container-logs.json Find Container uses `.includes()` for matching
+
+ Logs command works with fuzzy matching and Send Logs Response has valid chatId
+
+
+
+ Task 5: Handle "message not modified" error in logs refresh
+ n8n-workflow.json
+
+**Problem:** User reported "message is not modified" error when refreshing logs that haven't changed. Telegram API rejects editMessageText when content is identical.
+
+**Current flow:**
+```
+Prepare Inline Logs Input -> Execute Inline Logs -> Format Inline Logs Result -> Send Logs Result
+```
+
+**Send Logs Result (node id: http-send-logs-result):** Uses httpRequest to call editMessageText API.
+
+**Fix options:**
+A. Add timestamp to force content difference (changes UX - shows timestamp)
+B. Add error handling node after Send Logs Result to catch this specific error
+C. Use Telegram node with "Continue On Fail" and handle error in next node
+
+**Recommended: Option A - Add timestamp to logs display**
+
+This is the cleanest solution because:
+- Logs refresh SHOULD show when they were fetched
+- Always succeeds (no error handling needed)
+- Better UX - user knows logs are fresh
+
+**Implementation in Format Inline Logs Result (node id: b1800598-1ff6-4da3-8506-4e4e8127f902):**
+
+Update the jsCode to add timestamp to the message:
+
+```javascript
+// Format logs result for inline keyboard display
+const result = $json;
+const data = $('Prepare Inline Logs Input').item.json;
+
+const containerName = result.containerName;
+
+// Add timestamp to prevent "message not modified" error on refresh
+const timestamp = new Date().toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+});
+
+// Build inline keyboard
+const keyboard = [
+ [
+ { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },
+ { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }
+ ],
+ [
+ { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }
+ ]
+];
+
+// Append timestamp to message
+const messageWithTimestamp = result.message + `\n\nUpdated: ${timestamp}`;
+
+return {
+ json: {
+ chatId: data.chatId,
+ messageId: data.messageId,
+ text: messageWithTimestamp,
+ reply_markup: { inline_keyboard: keyboard }
+ }
+};
+```
+
+**Steps:**
+1. Find node with id "b1800598-1ff6-4da3-8506-4e4e8127f902" (Format Inline Logs Result)
+2. Update jsCode to add timestamp generation
+3. Append timestamp to the message text before returning
+
+
+- Format Inline Logs Result includes timestamp generation code
+- Output text includes "Updated: HH:MM:SS" suffix
+
+ Logs refresh always succeeds (timestamp ensures content is different)
+
+
+
+
+
+1. `grep -A5 '"Prepare Text Update Input"' n8n-workflow.json` shows only Execute Text Update connection
+2. `grep 'Build Progress Message' n8n-workflow.json | grep -c 'Prepare Batch'` returns 2 (update + action)
+3. `grep 'Prepare Text Logs Input' n8n-workflow.json` shows chatId reference in Send Logs Response
+4. `grep 'includes' n8n-container-logs.json` shows fuzzy matching in Find Container
+5. `grep 'timestamp' n8n-workflow.json` shows timestamp in Format Inline Logs Result
+
+
+
+- Text update sends single message (no race condition)
+- Batch update and batch actions execute without "undefined" errors
+- Logs command works with partial container names
+- Logs refresh never fails with "message not modified" error
+
+
+