docs(11): create phase plan — bitmap batch selection + update all button
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
---
|
||||
phase: 11-update-all-callback-limits
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- n8n-batch-ui.json
|
||||
- n8n-workflow.json
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Batch selection keyboard allows selecting 5+ containers without hitting 64-byte callback limit"
|
||||
- "Containers with long names (20+ chars) can be selected in batch keyboard"
|
||||
- "Batch navigation preserves selection state across pages"
|
||||
- "Batch exec buttons correctly pass selected container names to execution flow"
|
||||
- "Existing batch stop confirmation flow still works with new encoding"
|
||||
artifacts:
|
||||
- path: "n8n-batch-ui.json"
|
||||
provides: "Batch UI sub-workflow with bitmap-encoded selection state"
|
||||
contains: "toString(36)"
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Parse Callback Data with bitmap-aware batch parsing"
|
||||
contains: "parseInt.*36"
|
||||
key_links:
|
||||
- from: "n8n-workflow.json Parse Callback Data"
|
||||
to: "n8n-workflow.json Prepare Batch UI Input"
|
||||
via: "isBatchToggle/isBatchNav/isBatchExec flags + bitmap field"
|
||||
pattern: "isBatchToggle.*bitmap"
|
||||
- from: "n8n-workflow.json Prepare Batch UI Input"
|
||||
to: "n8n-batch-ui.json When executed by another workflow"
|
||||
via: "Execute Batch UI sub-workflow call"
|
||||
pattern: "bitmap"
|
||||
- from: "n8n-batch-ui.json Build Batch Keyboard"
|
||||
to: "Telegram API callback_data"
|
||||
via: "bitmap-encoded callback strings"
|
||||
pattern: "b:\\d:"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Replace CSV-in-callback batch selection encoding with bitmap-based encoding to eliminate the 64-byte Telegram callback_data limit.
|
||||
|
||||
Purpose: Current batch selection stores container names as CSV in callback_data (`batch:toggle:0:plex,sonarr,radarr:jellyfin`), which grows linearly with selection and hits the 64-byte Telegram API hard limit after selecting 2-3 containers. Bitmap encoding (`b:0:1a3:5`) uses a base36-encoded bitmask of container indices, keeping callback data under 30 bytes regardless of selection count.
|
||||
|
||||
Output: Modified n8n-batch-ui.json and n8n-workflow.json with bitmap-based batch selection supporting unlimited container selection.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/luc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@n8n-batch-ui.json
|
||||
@n8n-workflow.json
|
||||
@.planning/phases/11-update-all-callback-limits/11-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Implement bitmap encoding in batch UI sub-workflow</name>
|
||||
<files>n8n-batch-ui.json</files>
|
||||
<action>
|
||||
Modify ALL code nodes in n8n-batch-ui.json to use bitmap-encoded selection state instead of CSV-in-callback. The bitmap is a base36-encoded integer where each bit represents a container's selected state by its sorted index (running first, then alphabetical -- the existing sort order).
|
||||
|
||||
**Critical: n8n static data does NOT persist between workflow executions (Phase 10.2 finding). The bitmap must be entirely self-contained in callback_data -- no server-side state.**
|
||||
|
||||
**Callback data format changes:**
|
||||
|
||||
Old format -> New format:
|
||||
- Toggle: `batch:toggle:{page}:{selectedCsv}:{containerName}` -> `b:{page}:{bitmap}:{containerIndex}`
|
||||
- Nav: `batch:nav:{page}:{selectedCsv}` -> `bn:{bitmap}:{page}`
|
||||
- Exec: `batch:exec:{action}:{selectedCsv}` -> `be:{action}:{bitmap}`
|
||||
- Clear: `batch:clear` -> `batch:clear` (unchanged, no selection data needed)
|
||||
- Cancel: `batch:cancel` -> `batch:cancel` (unchanged)
|
||||
|
||||
**Bitmap encoding/decoding helpers (include as inline functions in each code node that needs them):**
|
||||
|
||||
```javascript
|
||||
// Encode: Set of indices -> base36 string
|
||||
function encodeBitmap(selectedIndices) {
|
||||
let bitmap = 0n;
|
||||
for (const idx of selectedIndices) {
|
||||
bitmap |= (1n << BigInt(idx));
|
||||
}
|
||||
return bitmap.toString(36);
|
||||
}
|
||||
|
||||
// Decode: base36 string -> Set of indices
|
||||
function decodeBitmap(b36) {
|
||||
if (!b36 || b36 === '0') return new Set();
|
||||
const bitmap = BigInt('0x0') + BigInt(parseInt(b36, 36)); // parse via BigInt
|
||||
// Actually: need proper base36 BigInt parse
|
||||
// Better approach: iterate chars
|
||||
let val = 0n;
|
||||
for (const ch of b36) {
|
||||
val = val * 36n + BigInt(parseInt(ch, 36));
|
||||
}
|
||||
const indices = new Set();
|
||||
let i = 0;
|
||||
let v = val;
|
||||
while (v > 0n) {
|
||||
if (v & 1n) indices.add(i);
|
||||
v >>= 1n;
|
||||
i++;
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Convert bitmap indices to container names using sorted container list
|
||||
function bitmapToNames(selectedIndices, sortedContainers) {
|
||||
return Array.from(selectedIndices)
|
||||
.filter(i => i < sortedContainers.length)
|
||||
.map(i => sortedContainers[i].name);
|
||||
}
|
||||
|
||||
// Convert container names to bitmap indices using sorted container list
|
||||
function namesToBitmap(names, sortedContainers) {
|
||||
const nameToIdx = new Map(sortedContainers.map((c, i) => [c.name, i]));
|
||||
const indices = new Set();
|
||||
for (const name of names) {
|
||||
if (nameToIdx.has(name)) indices.add(nameToIdx.get(name));
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
```
|
||||
|
||||
**IMPORTANT: Use BigInt for bitmap operations to handle >30 containers (standard JS numbers lose precision above 2^53).**
|
||||
|
||||
**Nodes to modify:**
|
||||
|
||||
1. **When executed by another workflow** -- Update input schema: replace `selectedCsv` field with `bitmap` field (type string). Keep `batchPage`, `toggleName`, `batchAction`.
|
||||
|
||||
2. **Build Batch Keyboard** -- Change callback_data generation:
|
||||
- Read `bitmap` from trigger data instead of `selectedCsv`
|
||||
- Decode bitmap to get selected indices
|
||||
- For each container row, use `b:{page}:{bitmap}:{globalIndex}` where globalIndex is the container's position in the full sorted list
|
||||
- For nav buttons, use `bn:{bitmap}:{targetPage}`
|
||||
- For exec buttons, use `be:{action}:{bitmap}`
|
||||
- Remove the 64-byte limit check (no longer needed -- max callback is ~20 bytes)
|
||||
- The `selectedCount` is calculated from bitmap (count set bits)
|
||||
|
||||
3. **Handle Toggle** -- Change from CSV manipulation to bitmap manipulation:
|
||||
- Read `bitmap` from trigger data instead of `selectedCsv`
|
||||
- Read `toggleName` (still passed from main workflow parser) -- BUT WAIT: with the new format, the main workflow sends `containerIndex` not `toggleName`. Actually, the main workflow's Parse Callback Data can resolve the index, but since the batch-ui sub-workflow fetches fresh container data anyway, we should send the index and let the sub-workflow resolve the name if needed.
|
||||
- Actually simpler: main workflow sends `containerIndex` (from callback data `b:{page}:{bitmap}:{index}`), and the batch-ui sub-workflow toggles the bit in the bitmap.
|
||||
- Toggle bit: XOR the bitmap with (1 << index)
|
||||
- Return updated bitmap string, selectedCount, and set `needsKeyboardUpdate: true`
|
||||
- REMOVE the limit_reached check entirely (bitmap encoding has no practical limit)
|
||||
|
||||
4. **Rebuild Keyboard After Toggle** -- Same keyboard building as Build Batch Keyboard but reads from Handle Toggle output:
|
||||
- Decode bitmap from Handle Toggle result
|
||||
- Build keyboard with `b:{page}:{newBitmap}:{globalIndex}` callbacks
|
||||
- Nav buttons: `bn:{newBitmap}:{targetPage}`
|
||||
- Exec buttons: `be:{action}:{newBitmap}`
|
||||
|
||||
5. **Handle Nav** -- Read `bitmap` instead of `selectedCsv`, pass through to rebuild
|
||||
|
||||
6. **Rebuild Keyboard For Nav** -- Same as #4 but reads from Handle Nav output
|
||||
|
||||
7. **Handle Clear** -- Return bitmap of '0' (empty) instead of empty selectedCsv
|
||||
|
||||
8. **Rebuild Keyboard After Clear** -- Build keyboard with bitmap '0'
|
||||
|
||||
9. **Handle Exec** -- Convert bitmap to container names for execution:
|
||||
- Decode bitmap to get selected indices
|
||||
- The sub-workflow needs the container list to resolve indices to names
|
||||
- Fetch containers (already done via Fetch Containers For Update) to map indices
|
||||
- BUT WAIT: Handle Exec doesn't fetch containers. It receives trigger data and returns action data. The exec flow in main workflow handles the actual container lookup.
|
||||
- For stop confirmation: the callback_data for bstop:confirm needs container names. With bitmap, use `bstop:confirm:{bitmap}:{timestamp}:kb` instead of `bstop:confirm:{namesCsv}:{timestamp}:kb`.
|
||||
- For immediate execution: return `action: 'execute'` with the bitmap. The main workflow will need to resolve bitmap to names using a container fetch.
|
||||
|
||||
10. **Handle Exec detail:** For the stop confirmation keyboard callback, change from `bstop:confirm:{selectedCsv}:{timestamp}:kb` to `bstop:confirm:{bitmap}:{timestamp}:kb`. This also needs the main workflow's Parse Callback Data to be updated for bstop parsing.
|
||||
|
||||
**ALSO: The Handle Exec node currently builds a stop confirmation message listing container names.** Since Handle Exec only receives the bitmap (not container names), it needs the container list to resolve names for the message text. However, looking at the current flow: Handle Exec -> Needs Keyboard Update? (false path) -> returns directly. The container names for the confirmation message come from... the exec callback `batch:exec:{action}:{selectedCsv}` which included the CSV. With bitmap, we need an alternative.
|
||||
|
||||
**Solution for Handle Exec:** The "exec" action in the batch-ui sub-workflow is preceded by a container fetch. Looking at the flow: Route Batch UI Action -> exec output -> Handle Exec. But Handle Exec doesn't have container data. In the current code, it reads `selectedCsv` from trigger data and splits by comma. With bitmap, it needs the container list.
|
||||
|
||||
**Better solution:** Add a Fetch Containers step before Handle Exec for the exec path, OR have the main workflow resolve bitmap to names before calling the sub-workflow for exec. Looking at the architecture: the simplest change is to have Handle Exec output the bitmap in its response, and have the MAIN WORKFLOW do the name resolution for the stop confirmation flow. But that changes the sub-workflow contract significantly.
|
||||
|
||||
**Simplest approach:** Keep the exec path working by having the batch-ui sub-workflow fetch containers when handling exec (add HTTP request to Docker API before Handle Exec on the exec path). This is what the toggle/nav/clear paths already do (they have Fetch Containers nodes).
|
||||
|
||||
Actually, reviewing the flow more carefully:
|
||||
- For `exec` action: Route Batch UI Action -> Handle Exec (code node, no container fetch)
|
||||
- For `toggle` action: Route Batch UI Action -> Handle Toggle -> Needs Keyboard Update? -> Fetch Containers For Update -> Rebuild Keyboard
|
||||
|
||||
So the exec path is the only one that doesn't fetch containers. Add a "Fetch Containers For Exec" HTTP node on the exec path between Route Batch UI Action and Handle Exec. The Handle Exec node will then have access to container data via `$input.all()`.
|
||||
|
||||
**New node to add:** "Fetch Containers For Exec" -- HTTP Request node identical to existing "Fetch Containers For Mode" (URL: `http://docker-socket-proxy:2375/containers/json?all=true`).
|
||||
|
||||
Wire: Route Batch UI Action [exec output] -> Fetch Containers For Exec -> Handle Exec
|
||||
|
||||
Modify Handle Exec to:
|
||||
- Read containers from `$input.all()` (from the new fetch node)
|
||||
- Read bitmap from trigger data
|
||||
- Decode bitmap to indices
|
||||
- Sort containers (same sort as Build Batch Keyboard)
|
||||
- Map indices to container names
|
||||
- Use resolved names for stop confirmation message and callback data
|
||||
- For immediate exec, return container names in the response
|
||||
|
||||
**Summary of all changes to n8n-batch-ui.json:**
|
||||
- Update trigger schema: `selectedCsv` -> `bitmap`, remove `batchPage` from toggle (page now embedded in callback), keep `toggleName` as `containerIndex` (number)
|
||||
- Actually, reconsider trigger schema: the main workflow's Prepare Batch UI Input will send the action-specific fields. For toggle: `containerIndex` and `bitmap`. For nav: `bitmap` and `batchPage`. For mode: `batchAction`. For exec: `bitmap` and `batchAction`. The trigger schema needs: chatId, messageId, queryId, callbackData, action, batchPage, bitmap, containerIndex, batchAction, correlationId.
|
||||
- Modify 8 existing code nodes
|
||||
- Add 1 new HTTP Request node (Fetch Containers For Exec)
|
||||
- Add 1 new connection (Route Batch UI Action exec -> Fetch Containers For Exec -> Handle Exec)
|
||||
</action>
|
||||
<verify>
|
||||
Load the modified n8n-batch-ui.json and verify:
|
||||
1. `python3 -c "import json; wf=json.load(open('n8n-batch-ui.json')); print(len(wf['nodes']), 'nodes')"` shows 17 nodes (16 original + 1 new Fetch Containers For Exec)
|
||||
2. All code nodes contain `toString(36)` or `BigInt` for bitmap operations
|
||||
3. No code node contains `selectedCsv` in callback_data strings
|
||||
4. All callback_data patterns match: `b:{page}:{bitmap}:{index}`, `bn:{bitmap}:{page}`, `be:{action}:{bitmap}`
|
||||
5. Handle Exec reads container data from $input and resolves bitmap to names
|
||||
</verify>
|
||||
<done>
|
||||
All 8 batch-ui code nodes use bitmap encoding. New Fetch Containers For Exec node added and wired. Callback data format uses `b:`, `bn:`, `be:` prefixes with base36 bitmap. No CSV-in-callback patterns remain. The 64-byte limit is no longer a constraint (max callback ~20 bytes for 50 containers).
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update main workflow callback parser and batch UI integration</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Modify the main workflow to parse bitmap-encoded batch callbacks and pass bitmap data to the batch-ui sub-workflow.
|
||||
|
||||
**1. Parse Callback Data node -- Update batch callback parsing:**
|
||||
|
||||
Replace the existing `batch:toggle:`, `batch:nav:`, `batch:exec:` parsers with bitmap-aware versions:
|
||||
|
||||
```javascript
|
||||
// NEW: Bitmap-encoded batch toggle: b:{page}:{bitmap}:{containerIndex}
|
||||
if (rawData.startsWith('b:') && !rawData.startsWith('batch:') && !rawData.startsWith('bstop:') && !rawData.startsWith('bexec:') && !rawData.startsWith('bn:') && !rawData.startsWith('be:')) {
|
||||
const parts = rawData.substring(2).split(':');
|
||||
const page = parseInt(parts[0]) || 0;
|
||||
const bitmap = parts[1] || '0';
|
||||
const containerIndex = parseInt(parts[2]);
|
||||
|
||||
return {
|
||||
json: {
|
||||
queryId, chatId, messageId,
|
||||
isBatchToggle: true,
|
||||
batchPage: page,
|
||||
bitmap: bitmap,
|
||||
containerIndex: containerIndex,
|
||||
isSelect: false, isList: false, isAction: false,
|
||||
isCancel: false, isBatch: false, expired: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: Bitmap-encoded batch nav: bn:{bitmap}:{page}
|
||||
if (rawData.startsWith('bn:')) {
|
||||
const parts = rawData.substring(3).split(':');
|
||||
const bitmap = parts[0] || '0';
|
||||
const page = parseInt(parts[1]) || 0;
|
||||
|
||||
return {
|
||||
json: {
|
||||
queryId, chatId, messageId,
|
||||
isBatchNav: true,
|
||||
batchPage: page,
|
||||
bitmap: bitmap,
|
||||
isSelect: false, isList: false, isAction: false,
|
||||
isCancel: false, isBatch: false, expired: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: Bitmap-encoded batch exec: be:{action}:{bitmap}
|
||||
if (rawData.startsWith('be:')) {
|
||||
const parts = rawData.substring(3).split(':');
|
||||
const action = parts[0];
|
||||
const bitmap = parts[1] || '0';
|
||||
|
||||
return {
|
||||
json: {
|
||||
queryId, chatId, messageId,
|
||||
isBatchExec: true,
|
||||
action: action,
|
||||
bitmap: bitmap,
|
||||
isSelect: false, isList: false, isAction: false,
|
||||
isCancel: false, isBatch: false, expired: false
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**IMPORTANT:** Place these new parsers BEFORE the old `batch:toggle:`, `batch:nav:`, `batch:exec:` parsers. Keep the old parsers as fallback for any in-flight messages that still use the old format (graceful migration). The old parsers should add `bitmap: '0'` to their output so downstream code doesn't break.
|
||||
|
||||
**Also update the `bstop:confirm` parser:** The callback data from batch-ui Handle Exec now uses `bstop:confirm:{bitmap}:{timestamp}:kb` instead of `bstop:confirm:{namesCsv}:{timestamp}:kb`. Update the parser to detect whether the middle segment is a bitmap (no commas, alphanumeric) or a CSV of names (contains commas). If bitmap, set `isBitmapBatchStop: true` and `bitmap` field.
|
||||
|
||||
**2. Prepare Batch UI Input node -- Pass bitmap instead of selectedCsv:**
|
||||
|
||||
Update the input preparation to forward bitmap data:
|
||||
|
||||
```javascript
|
||||
return {
|
||||
json: {
|
||||
chatId: data.chatId,
|
||||
messageId: data.messageId,
|
||||
queryId: data.queryId,
|
||||
callbackData: callbackData,
|
||||
action: action,
|
||||
batchAction: data.action || data.batchAction || 'start',
|
||||
batchPage: data.batchPage || 0,
|
||||
bitmap: data.bitmap || '0',
|
||||
containerIndex: data.containerIndex || 0,
|
||||
toggleName: data.toggleName || '',
|
||||
correlationId: $input.item.json.correlationId || ''
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Note: Keep `toggleName` for backward compat but add `containerIndex` for new format.
|
||||
|
||||
**3. Route Callback node -- Verify existing rules still work:**
|
||||
|
||||
The Route Callback switch node uses `isBatchToggle`, `isBatchNav`, `isBatchExec` flags which are still set by the new parsers. No changes needed to Route Callback rules.
|
||||
|
||||
**4. Handle the bstop:confirm with bitmap:**
|
||||
|
||||
When the main workflow receives `bstop:confirm:{bitmap}:{timestamp}:kb`, it needs to resolve the bitmap to container names. The existing flow is:
|
||||
- Route Callback [batchStopConfirm] -> Answer Batch Stop Confirm -> Check Batch Stop Expired -> Initialize Batch State -> ...
|
||||
|
||||
The Initialize Batch State node currently reads `containerNames` from Parse Callback Data. With bitmap, it needs to fetch containers and resolve.
|
||||
|
||||
**Solution:** Add bitmap detection in the bstop parser. If bitmap format detected, the Initialize Batch State node (or a preceding transform node) will need to fetch containers and resolve bitmap to names. However, this is complex and touches the batch stop confirmation flow.
|
||||
|
||||
**Simpler alternative for bstop:** Have the batch-ui Handle Exec node encode the container names in the confirmation message text (already done) and use a short CSV in the bstop callback since at the point of exec, we've already resolved names. The stop confirmation is only shown after the user clicks "Stop (N)" in the batch keyboard, at which point we can include names in the callback since they fit (the exec button is `be:stop:{bitmap}`, and the sub-workflow resolves bitmap to names, then builds the stop confirmation with names in the callback).
|
||||
|
||||
Wait -- the bstop callback data has the same 64-byte limit issue. If user selects 5+ containers with long names, `bstop:confirm:plex,sonarr,radarr,nzbget,jellyfin:{timestamp}:kb` will exceed 64 bytes.
|
||||
|
||||
**Solution:** Use bitmap in bstop too: `bstop:confirm:{bitmap}:{timestamp}:kb`. In the main workflow, when processing bstop:confirm with bitmap, add a container fetch + bitmap resolution step.
|
||||
|
||||
**Modify the Initialize Batch State node** to handle both formats:
|
||||
- If `containerNames` is an array (old format): use directly
|
||||
- If `bitmap` is set: fetch containers, sort, decode bitmap to indices, map to names
|
||||
|
||||
Actually, looking at the Initialize Batch State node more carefully: it receives data from the Check Batch Stop Expired node, which receives from Answer Batch Stop Confirm, which receives from Route Callback. The data flows through unchanged from Parse Callback Data.
|
||||
|
||||
The cleanest approach: Add an IF node after Check Batch Stop Expired that checks `isBitmapBatchStop`. If true, route to a new "Fetch Containers For Batch Stop" HTTP node -> "Resolve Bitmap To Names" code node -> then rejoin at Initialize Batch State. If false (legacy), go directly to Initialize Batch State.
|
||||
|
||||
This adds 3 nodes to the main workflow but cleanly handles both formats.
|
||||
|
||||
**New nodes to add to n8n-workflow.json:**
|
||||
1. "Is Bitmap Batch Stop" -- IF node checking `$json.bitmap` exists and is not empty
|
||||
2. "Fetch Containers For Bitmap Stop" -- HTTP Request to Docker API (same as Get All Containers For Update All)
|
||||
3. "Resolve Batch Stop Names" -- Code node that decodes bitmap, sorts containers, maps indices to names, outputs in same format as legacy flow (containerNames array)
|
||||
|
||||
Wire: Check Batch Stop Expired [false/not expired] -> Is Bitmap Batch Stop -> [true] -> Fetch Containers For Bitmap Stop -> Resolve Batch Stop Names -> Initialize Batch State
|
||||
-> [false] -> Initialize Batch State (existing connection)
|
||||
</action>
|
||||
<verify>
|
||||
Load the modified n8n-workflow.json and verify:
|
||||
1. `python3 -c "import json; wf=json.load(open('n8n-workflow.json')); print(len(wf['nodes']), 'nodes')"` shows 171 nodes (168 + 3 new bitmap resolution nodes)
|
||||
2. Parse Callback Data contains `b:` and `bn:` and `be:` parsers before old `batch:toggle:` etc.
|
||||
3. Prepare Batch UI Input passes `bitmap` field
|
||||
4. Old batch parsers still exist as fallback (graceful migration)
|
||||
5. bstop bitmap resolution flow exists (Is Bitmap Batch Stop -> Fetch -> Resolve -> Initialize)
|
||||
</verify>
|
||||
<done>
|
||||
Main workflow Parse Callback Data parses bitmap-encoded batch callbacks. Prepare Batch UI Input forwards bitmap to sub-workflow. Batch stop confirmation with bitmap resolved via container fetch. Old CSV-based parsers retained as fallback. Total: 171 nodes (168 + 3 new).
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Load both modified JSON files and verify they are valid JSON
|
||||
2. Verify no callback_data string in any code node exceeds 30 bytes for typical scenarios (10 containers, 5 selected)
|
||||
3. Verify bitmap encoding round-trips correctly: encode(decode(x)) === x for test values
|
||||
4. Count all nodes: n8n-batch-ui.json should have 17 nodes, n8n-workflow.json should have 171 nodes
|
||||
5. Verify all connections are valid (no dangling references)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Batch selection keyboard callback_data uses bitmap encoding (max ~20 bytes for 50 containers)
|
||||
- Toggle, nav, exec, clear, cancel all work with bitmap format
|
||||
- Old CSV format still parsed as fallback for in-flight messages
|
||||
- Batch stop confirmation works with bitmap-encoded selection
|
||||
- No 64-byte limit violations possible regardless of selection count or container name length
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-update-all-callback-limits/11-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,330 @@
|
||||
---
|
||||
phase: 11-update-all-callback-limits
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["11-01"]
|
||||
files_modified:
|
||||
- n8n-status.json
|
||||
- n8n-workflow.json
|
||||
autonomous: false
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can tap 'Update All :latest' button in container list inline keyboard"
|
||||
- "Tapping the button shows a confirmation dialog listing all :latest containers"
|
||||
- "User can type 'update all' to trigger the same confirmation flow"
|
||||
- "Confirming update-all triggers batch update execution for all :latest containers"
|
||||
artifacts:
|
||||
- path: "n8n-status.json"
|
||||
provides: "Container list keyboard with Update All button"
|
||||
contains: "uall:start"
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "uall:start callback routing to existing update-all flow"
|
||||
contains: "isUpdateAllStart"
|
||||
key_links:
|
||||
- from: "n8n-status.json Build Container List"
|
||||
to: "Telegram inline keyboard"
|
||||
via: "uall:start callback_data button"
|
||||
pattern: "uall:start"
|
||||
- from: "n8n-workflow.json Parse Callback Data"
|
||||
to: "n8n-workflow.json Route Callback"
|
||||
via: "isUpdateAllStart flag"
|
||||
pattern: "isUpdateAllStart"
|
||||
- from: "n8n-workflow.json Route Callback"
|
||||
to: "n8n-workflow.json Get All Containers For Update All"
|
||||
via: "updateallstart route"
|
||||
pattern: "updateallstart"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add "Update All :latest" inline keyboard button to the container list and wire its callback to the existing update-all confirmation flow. Then deploy all modified workflows and verify end-to-end functionality.
|
||||
|
||||
Purpose: BATCH-05 requires inline keyboard entry point for update-all. The text command path (BATCH-04) already works. This plan adds the inline button, routes its callback to the existing confirmation flow, deploys all changes (including Plan 01's bitmap encoding), and verifies everything works.
|
||||
|
||||
Output: Modified n8n-status.json with Update All button, modified n8n-workflow.json with uall:start routing, all workflows deployed and verified.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/luc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/11-update-all-callback-limits/11-01-SUMMARY.md
|
||||
@n8n-status.json
|
||||
@n8n-workflow.json
|
||||
@CLAUDE.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add Update All button to container list and wire callback routing</name>
|
||||
<files>n8n-status.json, n8n-workflow.json</files>
|
||||
<action>
|
||||
**1. n8n-status.json -- Build Container List node:**
|
||||
|
||||
Add an "Update All :latest" button row to the container list keyboard. Place it AFTER the navigation row and BEFORE the "Select Multiple" button. This button should appear on EVERY page of the container list.
|
||||
|
||||
In the Build Container List code node, after the navigation row block and before the "Select Multiple" button, add:
|
||||
|
||||
```javascript
|
||||
// Add Update All button
|
||||
keyboard.push([{
|
||||
text: '\u{1F504} Update All :latest',
|
||||
callback_data: 'uall:start'
|
||||
}]);
|
||||
```
|
||||
|
||||
The callback_data `uall:start` is only 10 bytes, well within the 64-byte limit.
|
||||
|
||||
Also add the same button to the Build Paginated List code node (used when paginating from an existing list view), in the same position relative to nav and Select Multiple.
|
||||
|
||||
Do NOT add to Build Container Submenu -- that is for single container actions only.
|
||||
|
||||
**2. n8n-workflow.json -- Parse Callback Data node:**
|
||||
|
||||
Add a parser for `uall:start` callback. Place it near the existing `uall:confirm` and `uall:cancel` parsers:
|
||||
|
||||
```javascript
|
||||
if (rawData === 'uall:start') {
|
||||
return {
|
||||
json: {
|
||||
queryId,
|
||||
chatId,
|
||||
messageId,
|
||||
isUpdateAllStart: true,
|
||||
isSelect: false,
|
||||
isList: false,
|
||||
isAction: false,
|
||||
isCancel: false,
|
||||
isBatch: false,
|
||||
isConfirm: false,
|
||||
expired: false
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**3. n8n-workflow.json -- Route Callback switch node:**
|
||||
|
||||
Add a new rule for `isUpdateAllStart` that routes to the update-all flow. Insert after the existing `updateallcancel` rule (index 1), before the `cancel` rule:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "is-update-all-start",
|
||||
"conditions": {
|
||||
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
|
||||
"conditions": [{
|
||||
"id": "update-all-start-true",
|
||||
"leftValue": "={{ $json.isUpdateAllStart }}",
|
||||
"rightValue": true,
|
||||
"operator": { "type": "boolean", "operation": "equals" }
|
||||
}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "updateallstart"
|
||||
}
|
||||
```
|
||||
|
||||
**4. n8n-workflow.json -- Add Answer Update All Start node:**
|
||||
|
||||
Add a new Telegram answerCallbackQuery node to acknowledge the button press before fetching containers. Use the same pattern as existing Answer nodes (e.g., Answer Batch UI Keyboard):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Answer Update All Start",
|
||||
"type": "n8n-nodes-base.telegram",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"operation": "callback",
|
||||
"callbackQueryId": "={{ $json.queryId }}",
|
||||
"additionalFields": {
|
||||
"text": "Checking for updates..."
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"telegramApi": {
|
||||
"id": "I0xTTiASl7C1NZhJ",
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wire: Route Callback [updateallstart output] -> Answer Update All Start -> Get All Containers For Update All
|
||||
|
||||
**5. n8n-workflow.json -- Fix Check Available Updates for dual origin:**
|
||||
|
||||
The Check Available Updates code node currently reads chatId from `$('Keyword Router').first().json.message.chat.id` which fails when coming from the callback path. Update it to handle both origins:
|
||||
|
||||
```javascript
|
||||
let chatId, messageId;
|
||||
try {
|
||||
const kwData = $('Keyword Router').first().json;
|
||||
chatId = kwData.message.chat.id;
|
||||
messageId = kwData.message.message_id;
|
||||
} catch(e) {
|
||||
// Inline keyboard origin -- data from Parse Callback Data
|
||||
const cbData = $('Parse Callback Data').first().json;
|
||||
chatId = cbData.chatId;
|
||||
messageId = cbData.messageId;
|
||||
}
|
||||
```
|
||||
|
||||
Keep the rest of the Check Available Updates logic unchanged (container filtering, :latest detection).
|
||||
|
||||
**6. Update connections in Route Callback:**
|
||||
|
||||
Add the new output connection at the correct index in the Route Callback connections array. The new `updateallstart` rule is inserted at index 2, so all existing connection indices after position 1 shift by 1. The new index 2 connection targets Answer Update All Start.
|
||||
|
||||
**Summary of changes:**
|
||||
- n8n-status.json: Add `uall:start` button to Build Container List and Build Paginated List
|
||||
- n8n-workflow.json: Add `uall:start` parser to Parse Callback Data
|
||||
- n8n-workflow.json: Add `updateallstart` rule to Route Callback (index 2, shifting existing rules)
|
||||
- n8n-workflow.json: Add "Answer Update All Start" Telegram node
|
||||
- n8n-workflow.json: Wire Route Callback [updateallstart] -> Answer Update All Start -> Get All Containers For Update All
|
||||
- n8n-workflow.json: Fix Check Available Updates to support callback origin (try/catch for Keyword Router)
|
||||
- n8n-workflow.json: Update connections array for Route Callback to include the new output
|
||||
</action>
|
||||
<verify>
|
||||
1. `python3 -c "import json; wf=json.load(open('n8n-status.json')); [print(n['name']) for n in wf['nodes']]"` shows same 11 nodes
|
||||
2. Build Container List code contains `uall:start`
|
||||
3. `python3 -c "import json; wf=json.load(open('n8n-workflow.json')); print(len(wf['nodes']), 'nodes')"` shows 172 nodes (171 from Plan 01 + 1 Answer Update All Start)
|
||||
4. Parse Callback Data contains `uall:start` parser
|
||||
5. Route Callback has `updateallstart` rule
|
||||
6. Connections exist: Route Callback -> Answer Update All Start -> Get All Containers For Update All
|
||||
</verify>
|
||||
<done>
|
||||
Container list keyboard shows "Update All :latest" button. Tapping it answers the callback, fetches containers, filters :latest, shows confirmation dialog. Existing confirmation/cancel/expired flows handle the rest. Check Available Updates works for both text and callback origins.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Deploy all workflows to n8n and run smoke test</name>
|
||||
<files>n8n-batch-ui.json, n8n-status.json, n8n-workflow.json</files>
|
||||
<action>
|
||||
Push all 3 modified workflow files to n8n using the push_workflow recipe from CLAUDE.md:
|
||||
|
||||
```bash
|
||||
. .env.n8n-api
|
||||
|
||||
push_workflow() {
|
||||
local FILE=$1 WF_ID=$2 WF_NAME=$3
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$FILE') as f:
|
||||
wf = json.load(f)
|
||||
payload = {'name': wf.get('name', '$WF_NAME'), 'nodes': wf['nodes'], 'connections': wf['connections'], 'settings': wf.get('settings', {})}
|
||||
if wf.get('staticData'): payload['staticData'] = wf['staticData']
|
||||
with open('/tmp/n8n-push-payload.json', 'w') as f:
|
||||
json.dump(payload, f)
|
||||
"
|
||||
local CODE=$(curl -s -o /tmp/n8n-push-result.txt -w "%{http_code}" \
|
||||
-X PUT "${N8N_HOST}/api/v1/workflows/${WF_ID}" \
|
||||
-H "X-N8N-API-KEY: ${N8N_API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/n8n-push-payload.json)
|
||||
echo " ${WF_NAME}: HTTP ${CODE}"
|
||||
}
|
||||
|
||||
push_workflow "n8n-workflow.json" "HmiXBlJefBRPMS0m4iNYc" "Main"
|
||||
push_workflow "n8n-batch-ui.json" "ZJhnGzJT26UUmW45" "Batch UI"
|
||||
push_workflow "n8n-status.json" "lqpg2CqesnKE2RJQ" "Status"
|
||||
```
|
||||
|
||||
All three should return HTTP 200.
|
||||
|
||||
After deployment, verify the main workflow is still active:
|
||||
```bash
|
||||
. .env.n8n-api; curl -s -o /tmp/n8n-result.txt -w "%{http_code}" \
|
||||
"${N8N_HOST}/api/v1/workflows/HmiXBlJefBRPMS0m4iNYc" \
|
||||
-H "X-N8N-API-KEY: ${N8N_API_KEY}" \
|
||||
&& python3 -c "
|
||||
import json
|
||||
with open('/tmp/n8n-result.txt') as f:
|
||||
wf = json.load(f)
|
||||
print(f'Active: {wf[\"active\"]}')
|
||||
print(f'Nodes: {len(wf[\"nodes\"])}')
|
||||
"
|
||||
```
|
||||
|
||||
If not active, activate:
|
||||
```bash
|
||||
. .env.n8n-api; curl -s -X POST "${N8N_HOST}/api/v1/workflows/HmiXBlJefBRPMS0m4iNYc/activate" \
|
||||
-H "X-N8N-API-KEY: ${N8N_API_KEY}"
|
||||
```
|
||||
</action>
|
||||
<verify>All three workflows deploy with HTTP 200. Main workflow is active.</verify>
|
||||
<done>All modified workflows pushed to n8n. Main workflow active and running.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: User acceptance testing of all Phase 11 features</name>
|
||||
<action>
|
||||
User verifies all four BATCH requirements via Telegram bot interaction. See verification steps below.
|
||||
</action>
|
||||
<verify>
|
||||
**Test 1 - Batch selection with 5+ containers (BATCH-06):**
|
||||
1. Send "status" to the bot
|
||||
2. Tap "Select Multiple" in the container list
|
||||
3. Select 5 or more containers by tapping them (checkmarks should appear)
|
||||
4. Verify you can navigate pages while selection is preserved
|
||||
5. Expected: All selections work, no "Maximum selection reached" errors
|
||||
|
||||
**Test 2 - Long container names (BATCH-07):**
|
||||
1. In batch selection mode, select containers with names 20+ characters
|
||||
2. Expected: Selection works without errors
|
||||
|
||||
**Test 3 - Batch execution:**
|
||||
1. Select 2-3 containers in batch mode
|
||||
2. Tap "Start (N)" or "Stop (N)"
|
||||
3. Expected: Stop shows confirmation, Start executes immediately
|
||||
4. Verify the actions complete successfully
|
||||
|
||||
**Test 4 - Update All from keyboard (BATCH-05):**
|
||||
1. Send "status" to the bot
|
||||
2. Look for "Update All :latest" button below the container list
|
||||
3. Tap the button
|
||||
4. Expected: Confirmation dialog showing :latest containers with Confirm/Cancel buttons
|
||||
5. Tap Cancel to verify cancel works
|
||||
|
||||
**Test 5 - Update All from text (BATCH-04):**
|
||||
1. Send "update all" to the bot
|
||||
2. Expected: Same confirmation dialog as Test 4
|
||||
|
||||
**Test 6 - Regression: single container actions:**
|
||||
1. Send "status", tap a container, tap Stop/Start/Restart
|
||||
2. Expected: All actions work as before
|
||||
</verify>
|
||||
<done>All six test scenarios pass. BATCH-04 through BATCH-07 requirements satisfied. No regressions in existing functionality.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. All four success criteria from ROADMAP met:
|
||||
- User can type "update all" (BATCH-04, pre-existing + verified)
|
||||
- User can tap "Update All" in keyboard (BATCH-05, new button + routing)
|
||||
- Batch selection supports 5+ containers (BATCH-06, bitmap encoding)
|
||||
- Long container names work (BATCH-07, bitmap encoding)
|
||||
2. No regression to existing functionality (single actions, batch stop/start, pagination, etc.)
|
||||
3. All workflows deployed and active in n8n
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Container list inline keyboard shows "Update All :latest" button
|
||||
- Tapping the button triggers confirmation dialog with :latest container list
|
||||
- Batch selection works with 5+ containers (no callback limit error)
|
||||
- Containers with long names selectable in batch mode
|
||||
- All existing bot functionality unaffected
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-update-all-callback-limits/11-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user