Update README and architecture diagram for v2.1.0

Add SaveSnapshot node to features list and architecture diagram,
update storage label from IndexedDB to Server Storage, add
maxNodeSnapshots setting to the table, and document the
node-triggered capture flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 22:10:48 +01:00
parent 11c6b7237b
commit 25b909f99f
3 changed files with 95 additions and 79 deletions

View File

@@ -5,7 +5,7 @@
<p align="center">
<a href="https://registry.comfy.org/publishers/ethanfel/nodes/comfyui-snapshot-manager"><img src="https://img.shields.io/badge/ComfyUI-Registry-blue?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMMyA3djEwbDkgNSA5LTVWN2wtOS01eiIgZmlsbD0id2hpdGUiLz48L3N2Zz4=" alt="ComfyUI Registry"/></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License"/></a>
<img src="https://img.shields.io/badge/version-2.0.0-blue" alt="Version"/>
<img src="https://img.shields.io/badge/version-2.1.0-blue" alt="Version"/>
<img src="https://img.shields.io/badge/ComfyUI-Extension-purple" alt="ComfyUI Extension"/>
</p>
@@ -27,6 +27,7 @@
- **Per-workflow storage** — Each workflow has its own independent snapshot history
- **Theme-aware UI** — Adapts to light and dark ComfyUI themes
- **Toast notifications** — Visual feedback for save, restore, and error operations
- **SaveSnapshot node** — Trigger snapshot captures from your workflow with a custom node; node snapshots are visually distinct (purple border + "Node" badge) and have their own rolling limit
- **Lock/pin snapshots** — Protect important snapshots from auto-pruning and "Clear All" with a single click
- **Concurrency-safe** — Lock guard prevents double-click issues during restore
- **Server-side storage** — Snapshots persist on the ComfyUI server's filesystem, accessible from any browser
@@ -105,6 +106,7 @@ All settings are available in **ComfyUI Settings > Snapshot Manager > Capture Se
| **Capture delay** | Slider | `3s` | Seconds to wait after the last edit before auto-capturing (130s) |
| **Max snapshots per workflow** | Slider | `50` | Maximum number of unlocked snapshots kept per workflow (5200). Oldest unlocked are pruned automatically; locked snapshots are never pruned |
| **Capture on workflow load** | Toggle | `On` | Save an "Initial" snapshot when a workflow is first loaded |
| **Max node-triggered snapshots** | Slider | `5` | Rolling limit for SaveSnapshot node captures per workflow (150). Node snapshots are pruned independently from auto/manual snapshots |
## Architecture
@@ -112,7 +114,7 @@ All settings are available in **ComfyUI Settings > Snapshot Manager > Capture Se
<img src="assets/architecture.png" alt="Architecture Diagram" width="100%"/>
</p>
**Data flow:**
**Auto/manual capture flow:**
1. **Graph edits** trigger a `graphChanged` event
2. A **debounce timer** prevents excessive writes
@@ -121,6 +123,13 @@ All settings are available in **ComfyUI Settings > Snapshot Manager > Capture Se
5. The **sidebar panel** fetches snapshots from the server and renders the snapshot list
6. **Restore/Swap** loads graph data back into ComfyUI with a lock guard to prevent concurrent operations
**Node-triggered capture flow:**
1. **SaveSnapshot node** executes during a queue prompt run
2. A **WebSocket event** is sent to the frontend, **skipping hash dedup** (the workflow doesn't change between runs)
3. The snapshot is saved with `source: "node"` and pruned against its own rolling limit (`maxNodeSnapshots`)
4. Node snapshots appear in the sidebar with a **purple left border** and **"Node" badge**
**Storage:** Snapshots are stored as JSON files on the server at `<extension_dir>/data/snapshots/<workflow_key>/<id>.json`. They persist across browser sessions, ComfyUI restarts, and are accessible from any browser connecting to the same server.
## FAQ

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -1,93 +1,100 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<defs>
<linearGradient id="abg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
</defs>
<rect width="800" height="420" rx="12" fill="url(#abg)"/>
<!-- Title -->
<text x="400" y="35" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="#94a3b8">How It Works</text>
<!-- Graph Edit box -->
<rect x="40" y="60" width="160" height="70" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="2"/>
<text x="120" y="92" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Graph Edit</text>
<text x="120" y="112" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">graphChanged event</text>
<!-- Arrow 1 -->
<line x1="200" y1="95" x2="250" y2="95" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Debounce box -->
<rect x="250" y="60" width="160" height="70" rx="8" fill="#1e293b" stroke="#f59e0b" stroke-width="2"/>
<text x="330" y="92" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Debounce Timer</text>
<text x="330" y="112" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">configurable delay</text>
<!-- Arrow 2 -->
<line x1="410" y1="95" x2="460" y2="95" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Hash Check box -->
<rect x="460" y="60" width="160" height="70" rx="8" fill="#1e293b" stroke="#8b5cf6" stroke-width="2"/>
<text x="540" y="92" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Hash Check</text>
<text x="540" y="112" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">per-workflow map</text>
<!-- Arrow 3 -->
<line x1="620" y1="95" x2="640" y2="95" stroke="#475569" stroke-width="2"/>
<line x1="640" y1="95" x2="640" y2="180" stroke="#475569" stroke-width="2"/>
<line x1="640" y1="180" x2="620" y2="180" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead-left)"/>
<!-- IndexedDB box -->
<rect x="460" y="150" width="160" height="70" rx="8" fill="#1e293b" stroke="#22c55e" stroke-width="2"/>
<text x="540" y="182" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">IndexedDB</text>
<text x="540" y="202" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">persistent storage</text>
<!-- Arrow 4 down to sidebar -->
<line x1="540" y1="220" x2="540" y2="265" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Sidebar Panel box (wide) -->
<rect x="250" y="265" width="370" height="130" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="2"/>
<text x="435" y="295" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" font-weight="600" fill="#e2e8f0">Sidebar Panel</text>
<!-- Sidebar sub-items -->
<rect x="270" y="310" width="100" height="32" rx="5" fill="#3b82f6" opacity="0.15" stroke="#3b82f6" stroke-width="1"/>
<text x="320" y="330" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#93c5fd">Take Snapshot</text>
<rect x="380" y="310" width="70" height="32" rx="5" fill="#22c55e" opacity="0.15" stroke="#22c55e" stroke-width="1"/>
<text x="415" y="330" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#86efac">Restore</text>
<rect x="460" y="310" width="55" height="32" rx="5" fill="#f59e0b" opacity="0.15" stroke="#f59e0b" stroke-width="1"/>
<text x="488" y="330" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#fcd34d">Swap</text>
<rect x="525" y="310" width="70" height="32" rx="5" fill="#8b5cf6" opacity="0.15" stroke="#8b5cf6" stroke-width="1"/>
<text x="560" y="330" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#c4b5fd">Search</text>
<text x="435" y="375" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">toast notifications &#183; confirm dialogs &#183; loading states</text>
<!-- Restore arrow back up -->
<rect x="40" y="180" width="160" height="70" rx="8" fill="#1e293b" stroke="#22c55e" stroke-width="2"/>
<text x="120" y="207" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Restore / Swap</text>
<text x="120" y="227" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">with lock guard</text>
<line x1="250" y1="330" x2="200" y2="265" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-green)"/>
<line x1="120" y1="180" x2="120" y2="130" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-green)"/>
<text x="120" y="155" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#22c55e">loadGraphData</text>
<!-- Manual capture arrow -->
<line x1="320" y1="310" x2="460" y2="185" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-blue)"/>
<!-- Arrowhead markers -->
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#475569"/>
</marker>
<marker id="arrowhead-left" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto-start-reverse">
<polygon points="10 0, 0 3.5, 10 7" fill="#475569"/>
</marker>
<marker id="arrowhead-green" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22c55e"/>
</marker>
<marker id="arrowhead-blue" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6"/>
</marker>
<marker id="arrowhead-purple" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#6d28d9"/>
</marker>
</defs>
<rect width="800" height="450" rx="12" fill="url(#abg)"/>
<!-- Title -->
<text x="400" y="35" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="#94a3b8">How It Works</text>
<!-- ═══ Row 1: Auto-capture path ═══ -->
<rect x="40" y="55" width="160" height="70" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="2"/>
<text x="120" y="87" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Graph Edit</text>
<text x="120" y="107" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">graphChanged event</text>
<line x1="200" y1="90" x2="250" y2="90" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<rect x="250" y="55" width="160" height="70" rx="8" fill="#1e293b" stroke="#f59e0b" stroke-width="2"/>
<text x="330" y="87" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Debounce Timer</text>
<text x="330" y="107" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">configurable delay</text>
<line x1="410" y1="90" x2="460" y2="90" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<rect x="460" y="55" width="160" height="70" rx="8" fill="#1e293b" stroke="#8b5cf6" stroke-width="2"/>
<text x="540" y="87" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Hash Check</text>
<text x="540" y="107" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">per-workflow dedup</text>
<!-- Arrow: Hash Check → Server Storage (bend down) -->
<line x1="620" y1="90" x2="655" y2="90" stroke="#475569" stroke-width="2"/>
<line x1="655" y1="90" x2="655" y2="160" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- ═══ Row 2: Node path + Server Storage ═══ -->
<rect x="40" y="165" width="170" height="70" rx="8" fill="#1e293b" stroke="#6d28d9" stroke-width="2"/>
<text x="125" y="197" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">SaveSnapshot Node</text>
<text x="125" y="217" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">queue prompt trigger</text>
<!-- Arrow: SaveSnapshot → Server Storage with label -->
<line x1="210" y1="200" x2="545" y2="200" stroke="#6d28d9" stroke-width="2" marker-end="url(#arrowhead-purple)"/>
<text x="378" y="192" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#a78bfa">WS event &#183; skips hash dedup</text>
<rect x="545" y="160" width="220" height="80" rx="8" fill="#1e293b" stroke="#22c55e" stroke-width="2"/>
<text x="655" y="195" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Server Storage</text>
<text x="655" y="215" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">JSON files &#183; per-workflow dirs</text>
<!-- Arrow: Server Storage → Sidebar -->
<line x1="655" y1="240" x2="655" y2="285" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- ═══ Row 3: Sidebar + Restore ═══ -->
<rect x="250" y="285" width="515" height="130" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="2"/>
<text x="507" y="313" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" font-weight="600" fill="#e2e8f0">Sidebar Panel</text>
<rect x="270" y="327" width="100" height="32" rx="5" fill="#3b82f6" opacity="0.15" stroke="#3b82f6" stroke-width="1"/>
<text x="320" y="347" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#93c5fd">Take Snapshot</text>
<rect x="380" y="327" width="70" height="32" rx="5" fill="#22c55e" opacity="0.15" stroke="#22c55e" stroke-width="1"/>
<text x="415" y="347" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#86efac">Restore</text>
<rect x="460" y="327" width="55" height="32" rx="5" fill="#f59e0b" opacity="0.15" stroke="#f59e0b" stroke-width="1"/>
<text x="488" y="347" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#fcd34d">Swap</text>
<rect x="525" y="327" width="70" height="32" rx="5" fill="#8b5cf6" opacity="0.15" stroke="#8b5cf6" stroke-width="1"/>
<text x="560" y="347" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#c4b5fd">Search</text>
<rect x="605" y="327" width="55" height="32" rx="5" fill="#6d28d9" opacity="0.15" stroke="#6d28d9" stroke-width="1"/>
<text x="633" y="347" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#a78bfa">Lock</text>
<text x="507" y="395" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">toast notifications &#183; confirm dialogs &#183; loading states</text>
<!-- Restore/Swap box -->
<rect x="40" y="305" width="160" height="70" rx="8" fill="#1e293b" stroke="#22c55e" stroke-width="2"/>
<text x="120" y="337" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#e2e8f0">Restore / Swap</text>
<text x="120" y="357" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="#64748b">with lock guard</text>
<!-- Dashed: Sidebar → Restore -->
<line x1="250" y1="350" x2="200" y2="340" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-green)"/>
<!-- Dashed: Restore → Graph Edit (routed around left side) -->
<path d="M 40 335 H 20 V 90 H 40" fill="none" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-green)"/>
<text x="11" y="250" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#22c55e" transform="rotate(-90, 11, 250)">loadGraphData</text>
<!-- Dashed: Take Snapshot → Server Storage -->
<line x1="320" y1="327" x2="558" y2="240" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="6,3" marker-end="url(#arrowhead-blue)"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB