Compare commits

...

6 Commits

Author SHA1 Message Date
Bas Nijholt
43de1ba8a9 Add additional SVG logo proposals 2026-01-05 01:31:24 -08:00
Bas Nijholt
b969c749be Add SVG recreation of current logo 2026-01-05 01:20:31 -08:00
Bas Nijholt
cfba027df6 Add minimal/geometric logo proposals
Eight new concepts with cleaner, modern aesthetic:
- Hexagon grid (honeycomb cluster)
- Container sprout (simple two-leaf)
- Stacked growth (containers + arrow)
- Negative space (plant cutout)
- CF monogram
- Abstract barn silhouette
- Dots grid (distributed containers)
- Single mark (container + corner leaf)
2026-01-05 01:17:42 -08:00
Bas Nijholt
18e5cc653b Add logo proposal SVGs
Six logo concepts combining farm imagery with Docker containers:
- Barn with container doors
- Container with wheat growing
- Silos styled as stacked containers
- Tractor pulling container trailer
- Harvest crate with container elements
- Minimal icon (container + plant)
2026-01-05 01:13:47 -08:00
Bas Nijholt
8dabc27272 update: Only restart containers when images change (#143)
* update: Only restart containers when images change

Use `up -d --pull always --build` instead of separate pull/build/down/up
steps. This avoids unnecessary container restarts when images haven't
changed.

* Update README.md

* docs: Update update command description across all docs

Reflect new behavior: only recreates containers if images changed.

* Update README.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-05 10:06:45 +01:00
Bas Nijholt
5e08f1d712 web: Exclude web stack from Update All button (#142) 2026-01-04 19:56:41 +01:00
32 changed files with 918 additions and 141 deletions

View File

@@ -138,7 +138,7 @@ CLI available as `cf` or `compose-farm`.
| `stop` | Stop services without removing containers (`docker compose stop`) |
| `pull` | Pull latest images |
| `restart` | `down` + `up -d` |
| `update` | `pull` + `build` + `down` + `up -d` |
| `update` | Pull, build, recreate only if changed (`up -d --pull always --build`) |
| `apply` | Make reality match config: migrate stacks + stop orphans. Use `--dry-run` to preview |
| `compose` | Run any docker compose command on a stack (passthrough) |
| `logs` | Show stack logs |

View File

@@ -370,7 +370,7 @@ The CLI is available as both `compose-farm` and the shorter `cf` alias.
| `cf down <stack>` | Stop and remove stack containers |
| `cf stop <stack>` | Stop stack without removing containers |
| `cf restart <stack>` | down + up |
| `cf update <stack>` | pull + build + down + up |
| `cf update <stack>` | Pull, build, recreate only if changed |
| `cf pull <stack>` | Pull latest images |
| `cf logs -f <stack>` | Follow logs |
| `cf ps` | Show status of all stacks |
@@ -403,7 +403,7 @@ cf pull --all
# Restart (down + up)
cf restart plex
# Update (pull + build + down + up) - the end-to-end update command
# Update (pull + build, only recreates containers if images changed)
cf update --all
# Update state from reality (discovers running stacks + captures digests)
@@ -475,8 +475,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
│ pull Pull latest images (docker compose pull). │
│ restart Restart stacks (down + up). With --service, restarts just │
│ that service. │
│ update Update stacks (pull + build + down + up). With --service,
│ updates just that service. │
│ update Update stacks. Only recreates containers if images changed.
│ apply Make reality match config (start, migrate, stop │
│ strays/orphans as needed). │
│ compose Run any docker compose command on a stack. │
@@ -694,8 +693,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
Usage: cf update [OPTIONS] [STACKS]...
Update stacks (pull + build + down + up). With --service, updates just that
service.
Update stacks. Only recreates containers if images changed.
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ stacks [STACKS]... Stacks to operate on │

View File

@@ -15,7 +15,7 @@ The Compose Farm CLI is available as both `compose-farm` and the shorter alias `
| | `down` | Stop stacks |
| | `stop` | Stop services without removing containers |
| | `restart` | Restart stacks (down + up) |
| | `update` | Update stacks (pull + build + down + up) |
| | `update` | Update stacks (only recreates if images changed) |
| | `pull` | Pull latest images |
| | `compose` | Run any docker compose command |
| **Monitoring** | `ps` | Show stack status |
@@ -225,7 +225,7 @@ cf restart immich --service database
### cf update
Update stacks (pull + build + down + up). With `--service`, updates just that service.
Update stacks. Only recreates containers if images changed. With `--service`, updates just that service.
<video autoplay loop muted playsinline>
<source src="/assets/update.webm" type="video/webm">

View File

@@ -1,5 +1,5 @@
# Update Demo
# Shows updating stacks (pull + build + down + up)
# Shows updating stacks (only recreates containers if images changed)
Output docs/assets/update.gif
Output docs/assets/update.webm

View File

@@ -329,7 +329,7 @@ cf apply
```bash
cf update --all
# Runs: pull + build + down + up for each stack
# Only recreates containers if images changed
```
## Next Steps

View File

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Barn with Container Doors -->
<defs>
<linearGradient id="barnRed" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#D94B4B"/>
<stop offset="100%" style="stop-color:#B33A3A"/>
</linearGradient>
<linearGradient id="roofRed" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#C44040"/>
<stop offset="100%" style="stop-color:#9E2E2E"/>
</linearGradient>
</defs>
<!-- Barn body -->
<rect x="40" y="90" width="120" height="80" fill="url(#barnRed)" rx="2"/>
<!-- Barn roof -->
<polygon points="30,90 100,40 170,90" fill="url(#roofRed)"/>
<!-- Roof cupola -->
<rect x="90" y="35" width="20" height="15" fill="#8B4513"/>
<polygon points="85,35 100,20 115,35" fill="#A0522D"/>
<!-- Container doors (left) -->
<rect x="48" y="105" width="45" height="58" fill="#2496ED" rx="2"/>
<rect x="50" y="107" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="50" y="121" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="50" y="135" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="50" y="149" width="41" height="12" fill="#1A7AC7" rx="1"/>
<!-- Container doors (right) -->
<rect x="107" y="105" width="45" height="58" fill="#2496ED" rx="2"/>
<rect x="109" y="107" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="109" y="121" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="109" y="135" width="41" height="12" fill="#1A7AC7" rx="1"/>
<rect x="109" y="149" width="41" height="12" fill="#1A7AC7" rx="1"/>
<!-- Door handles -->
<circle cx="90" cy="134" r="3" fill="#F0F0F0"/>
<circle cx="110" cy="134" r="3" fill="#F0F0F0"/>
<!-- Ground -->
<ellipse cx="100" cy="175" rx="70" ry="8" fill="#5D8A3E" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Container with Wheat Growing -->
<defs>
<linearGradient id="containerBlue" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="100%" style="stop-color:#1A7AC7"/>
</linearGradient>
<linearGradient id="wheat" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F4D03F"/>
<stop offset="100%" style="stop-color:#D4AC0D"/>
</linearGradient>
</defs>
<!-- Container base -->
<rect x="35" y="110" width="130" height="70" fill="url(#containerBlue)" rx="4"/>
<!-- Container ridges -->
<rect x="40" y="115" width="120" height="14" fill="#1A7AC7" rx="2"/>
<rect x="40" y="133" width="120" height="14" fill="#1A7AC7" rx="2"/>
<rect x="40" y="151" width="120" height="14" fill="#1A7AC7" rx="2"/>
<rect x="40" y="169" width="120" height="8" fill="#1A7AC7" rx="2"/>
<!-- Container corner posts -->
<rect x="35" y="110" width="8" height="70" fill="#1565C0"/>
<rect x="157" y="110" width="8" height="70" fill="#1565C0"/>
<!-- Wheat stalks -->
<g stroke="#8B7355" stroke-width="2" fill="none">
<path d="M70 110 Q68 80 70 50"/>
<path d="M100 110 Q100 70 100 35"/>
<path d="M130 110 Q132 80 130 50"/>
</g>
<!-- Wheat heads (left) -->
<g fill="url(#wheat)">
<ellipse cx="70" cy="50" rx="4" ry="8"/>
<ellipse cx="65" cy="55" rx="4" ry="7"/>
<ellipse cx="75" cy="55" rx="4" ry="7"/>
<ellipse cx="63" cy="62" rx="3" ry="6"/>
<ellipse cx="77" cy="62" rx="3" ry="6"/>
</g>
<!-- Wheat heads (center - taller) -->
<g fill="url(#wheat)">
<ellipse cx="100" cy="35" rx="5" ry="9"/>
<ellipse cx="94" cy="42" rx="4" ry="8"/>
<ellipse cx="106" cy="42" rx="4" ry="8"/>
<ellipse cx="91" cy="51" rx="4" ry="7"/>
<ellipse cx="109" cy="51" rx="4" ry="7"/>
<ellipse cx="94" cy="60" rx="3" ry="6"/>
<ellipse cx="106" cy="60" rx="3" ry="6"/>
</g>
<!-- Wheat heads (right) -->
<g fill="url(#wheat)">
<ellipse cx="130" cy="50" rx="4" ry="8"/>
<ellipse cx="125" cy="55" rx="4" ry="7"/>
<ellipse cx="135" cy="55" rx="4" ry="7"/>
<ellipse cx="123" cy="62" rx="3" ry="6"/>
<ellipse cx="137" cy="62" rx="3" ry="6"/>
</g>
<!-- Small leaves on stalks -->
<g fill="#6B8E23">
<path d="M70 85 Q60 80 70 75"/>
<path d="M70 75 Q80 70 70 65"/>
<path d="M100 90 Q88 85 100 80"/>
<path d="M100 75 Q112 70 100 65"/>
<path d="M130 85 Q140 80 130 75"/>
<path d="M130 75 Q120 70 130 65"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Silos as Stacked Containers -->
<defs>
<linearGradient id="silo1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="50%" style="stop-color:#4AA8F2"/>
<stop offset="100%" style="stop-color:#2496ED"/>
</linearGradient>
<linearGradient id="silo2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#1E88E5"/>
<stop offset="50%" style="stop-color:#42A5F5"/>
<stop offset="100%" style="stop-color:#1E88E5"/>
</linearGradient>
<linearGradient id="silo3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#1976D2"/>
<stop offset="50%" style="stop-color:#2196F3"/>
<stop offset="100%" style="stop-color:#1976D2"/>
</linearGradient>
</defs>
<!-- Left silo (shortest) -->
<rect x="25" y="100" width="40" height="75" fill="url(#silo1)" rx="3"/>
<ellipse cx="45" cy="100" rx="20" ry="8" fill="#4AA8F2"/>
<ellipse cx="45" cy="175" rx="20" ry="5" fill="#1565C0"/>
<!-- Container ridges -->
<path d="M25 115 h40 M25 130 h40 M25 145 h40 M25 160 h40" stroke="#1A7AC7" stroke-width="2"/>
<!-- Roof dome -->
<ellipse cx="45" cy="100" rx="18" ry="12" fill="#1565C0"/>
<ellipse cx="45" cy="98" rx="16" ry="10" fill="#2196F3"/>
<!-- Middle silo (tallest) -->
<rect x="80" y="55" width="45" height="120" fill="url(#silo2)" rx="3"/>
<ellipse cx="102.5" cy="55" rx="22.5" ry="10" fill="#42A5F5"/>
<ellipse cx="102.5" cy="175" rx="22.5" ry="5" fill="#1565C0"/>
<!-- Container ridges -->
<path d="M80 75 h45 M80 95 h45 M80 115 h45 M80 135 h45 M80 155 h45" stroke="#1976D2" stroke-width="2"/>
<!-- Roof dome -->
<ellipse cx="102.5" cy="55" rx="20" ry="14" fill="#1565C0"/>
<ellipse cx="102.5" cy="52" rx="18" ry="12" fill="#2196F3"/>
<!-- Cupola -->
<rect x="97" y="35" width="11" height="17" fill="#1565C0"/>
<polygon points="92,35 102.5,22 113,35" fill="#1976D2"/>
<!-- Right silo (medium) -->
<rect x="140" y="85" width="38" height="90" fill="url(#silo3)" rx="3"/>
<ellipse cx="159" cy="85" rx="19" ry="8" fill="#2196F3"/>
<ellipse cx="159" cy="175" rx="19" ry="5" fill="#1565C0"/>
<!-- Container ridges -->
<path d="M140 100 h38 M140 115 h38 M140 130 h38 M140 145 h38 M140 160 h38" stroke="#1565C0" stroke-width="2"/>
<!-- Roof dome -->
<ellipse cx="159" cy="85" rx="17" ry="11" fill="#1565C0"/>
<ellipse cx="159" cy="83" rx="15" ry="9" fill="#2196F3"/>
<!-- Ground -->
<ellipse cx="100" cy="180" rx="85" ry="10" fill="#5D8A3E" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Minimalist Tractor with Container Trailer -->
<defs>
<linearGradient id="tractorGreen" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4CAF50"/>
<stop offset="100%" style="stop-color:#388E3C"/>
</linearGradient>
<linearGradient id="containerBlue2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="100%" style="stop-color:#1976D2"/>
</linearGradient>
</defs>
<!-- Container trailer -->
<rect x="90" y="95" width="85" height="50" fill="url(#containerBlue2)" rx="3"/>
<!-- Container ridges -->
<rect x="95" y="100" width="75" height="10" fill="#1A7AC7" rx="1"/>
<rect x="95" y="113" width="75" height="10" fill="#1A7AC7" rx="1"/>
<rect x="95" y="126" width="75" height="10" fill="#1A7AC7" rx="1"/>
<!-- Corner posts -->
<rect x="90" y="95" width="6" height="50" fill="#1565C0"/>
<rect x="169" y="95" width="6" height="50" fill="#1565C0"/>
<!-- Trailer wheels -->
<circle cx="115" cy="155" r="12" fill="#333"/>
<circle cx="115" cy="155" r="6" fill="#666"/>
<circle cx="150" cy="155" r="12" fill="#333"/>
<circle cx="150" cy="155" r="6" fill="#666"/>
<!-- Trailer hitch bar -->
<rect x="55" y="125" width="40" height="6" fill="#555"/>
<!-- Tractor body -->
<rect x="20" y="90" width="50" height="40" fill="url(#tractorGreen)" rx="5"/>
<!-- Tractor hood -->
<rect x="15" y="100" width="20" height="25" fill="url(#tractorGreen)" rx="3"/>
<!-- Tractor cabin -->
<rect x="35" y="70" width="30" height="25" fill="#333" rx="3"/>
<rect x="38" y="73" width="24" height="18" fill="#87CEEB" rx="2"/>
<!-- Exhaust pipe -->
<rect x="18" y="80" width="5" height="20" fill="#444" rx="1"/>
<!-- Big rear wheel -->
<circle cx="55" cy="145" r="22" fill="#333"/>
<circle cx="55" cy="145" r="16" fill="#444"/>
<circle cx="55" cy="145" r="8" fill="#666"/>
<!-- Wheel treads -->
<g stroke="#555" stroke-width="3">
<line x1="55" y1="125" x2="55" y2="130"/>
<line x1="55" y1="160" x2="55" y2="165"/>
<line x1="35" y1="145" x2="40" y2="145"/>
<line x1="70" y1="145" x2="75" y2="145"/>
<line x1="40" y1="130" x2="43" y2="133"/>
<line x1="67" y1="157" x2="70" y2="160"/>
<line x1="40" y1="160" x2="43" y2="157"/>
<line x1="67" y1="133" x2="70" y2="130"/>
</g>
<!-- Small front wheel -->
<circle cx="22" cy="145" r="12" fill="#333"/>
<circle cx="22" cy="145" r="8" fill="#444"/>
<circle cx="22" cy="145" r="4" fill="#666"/>
<!-- Ground line -->
<line x1="5" y1="167" x2="195" y2="167" stroke="#5D8A3E" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Harvest Crate / Container Hybrid -->
<defs>
<linearGradient id="wood" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#DEB887"/>
<stop offset="100%" style="stop-color:#B8860B"/>
</linearGradient>
<linearGradient id="blueAccent" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="100%" style="stop-color:#1976D2"/>
</linearGradient>
</defs>
<!-- Crate back panel (3D effect) -->
<polygon points="45,60 155,60 175,45 65,45" fill="#A0522D"/>
<polygon points="155,60 155,150 175,135 175,45" fill="#8B4513"/>
<!-- Main crate body -->
<rect x="45" y="60" width="110" height="90" fill="url(#wood)" rx="3"/>
<!-- Horizontal slats -->
<rect x="45" y="60" width="110" height="18" fill="#C9A86C" rx="2"/>
<rect x="45" y="82" width="110" height="18" fill="#C9A86C" rx="2"/>
<rect x="45" y="104" width="110" height="18" fill="#C9A86C" rx="2"/>
<rect x="45" y="126" width="110" height="18" fill="#C9A86C" rx="2"/>
<!-- Slat gaps (darker lines) -->
<line x1="45" y1="78" x2="155" y2="78" stroke="#8B7355" stroke-width="2"/>
<line x1="45" y1="100" x2="155" y2="100" stroke="#8B7355" stroke-width="2"/>
<line x1="45" y1="122" x2="155" y2="122" stroke="#8B7355" stroke-width="2"/>
<line x1="45" y1="144" x2="155" y2="144" stroke="#8B7355" stroke-width="2"/>
<!-- Blue container-style corner brackets -->
<path d="M45 60 L45 90 L55 90 L55 70 L75 70 L75 60 Z" fill="url(#blueAccent)"/>
<path d="M155 60 L155 90 L145 90 L145 70 L125 70 L125 60 Z" fill="url(#blueAccent)"/>
<path d="M45 150 L45 120 L55 120 L55 140 L75 140 L75 150 Z" fill="url(#blueAccent)"/>
<path d="M155 150 L155 120 L145 120 L145 140 L125 140 L125 150 Z" fill="url(#blueAccent)"/>
<!-- Docker/container symbol in center -->
<g transform="translate(100, 105)">
<rect x="-25" y="-15" width="50" height="30" fill="#2496ED" rx="3" opacity="0.9"/>
<!-- Container boxes inside -->
<rect x="-22" y="-12" width="12" height="8" fill="#fff" rx="1"/>
<rect x="-7" y="-12" width="12" height="8" fill="#fff" rx="1"/>
<rect x="8" y="-12" width="12" height="8" fill="#fff" rx="1"/>
<rect x="-22" y="0" width="12" height="8" fill="#fff" rx="1"/>
<rect x="-7" y="0" width="12" height="8" fill="#fff" rx="1"/>
<rect x="8" y="0" width="12" height="8" fill="#fff" rx="1"/>
</g>
<!-- Rope handles -->
<ellipse cx="35" cy="100" rx="8" ry="15" fill="none" stroke="#8B7355" stroke-width="4"/>
<ellipse cx="165" cy="100" rx="8" ry="15" fill="none" stroke="#8B7355" stroke-width="4"/>
<!-- Ground shadow -->
<ellipse cx="100" cy="160" rx="60" ry="8" fill="#333" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Minimal Icon: Container with sprouting plant -->
<defs>
<linearGradient id="blueGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="100%" style="stop-color:#1565C0"/>
</linearGradient>
<linearGradient id="greenGrad" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" style="stop-color:#4CAF50"/>
<stop offset="100%" style="stop-color:#81C784"/>
</linearGradient>
</defs>
<!-- Container -->
<rect x="40" y="100" width="120" height="70" fill="url(#blueGrad)" rx="6"/>
<!-- Container ridges -->
<g fill="#1A7AC7">
<rect x="48" y="108" width="104" height="12" rx="2"/>
<rect x="48" y="124" width="104" height="12" rx="2"/>
<rect x="48" y="140" width="104" height="12" rx="2"/>
<rect x="48" y="156" width="104" height="10" rx="2"/>
</g>
<!-- Corner posts -->
<rect x="40" y="100" width="10" height="70" fill="#0D47A1" rx="2"/>
<rect x="150" y="100" width="10" height="70" fill="#0D47A1" rx="2"/>
<!-- Plant stem -->
<path d="M100 100 Q100 75 100 50" stroke="#4CAF50" stroke-width="6" fill="none" stroke-linecap="round"/>
<!-- Leaves -->
<ellipse cx="85" cy="65" rx="15" ry="8" fill="url(#greenGrad)" transform="rotate(-30 85 65)"/>
<ellipse cx="115" cy="65" rx="15" ry="8" fill="url(#greenGrad)" transform="rotate(30 115 65)"/>
<ellipse cx="80" cy="80" rx="12" ry="6" fill="url(#greenGrad)" transform="rotate(-45 80 80)"/>
<ellipse cx="120" cy="80" rx="12" ry="6" fill="url(#greenGrad)" transform="rotate(45 120 80)"/>
<!-- Top leaves (crown) -->
<ellipse cx="100" cy="45" rx="10" ry="18" fill="url(#greenGrad)"/>
<ellipse cx="90" cy="50" rx="8" ry="14" fill="#66BB6A" transform="rotate(-20 90 50)"/>
<ellipse cx="110" cy="50" rx="8" ry="14" fill="#66BB6A" transform="rotate(20 110 50)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Hexagon grid: containers as honeycomb/farm cells -->
<defs>
<linearGradient id="hex1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2496ED"/>
<stop offset="100%" style="stop-color:#1976D2"/>
</linearGradient>
<linearGradient id="hex2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4CAF50"/>
<stop offset="100%" style="stop-color:#388E3C"/>
</linearGradient>
</defs>
<!-- Hexagon cluster -->
<g transform="translate(100, 100)">
<!-- Center hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)"/>
<!-- Top hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(0, -52)" opacity="0.8"/>
<!-- Top-right hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(45, -26)" opacity="0.6"/>
<!-- Bottom-right hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(45, 26)" opacity="0.7"/>
<!-- Bottom hex - green accent -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex2)" transform="translate(0, 52)"/>
<!-- Bottom-left hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(-45, 26)" opacity="0.7"/>
<!-- Top-left hex -->
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(-45, -26)" opacity="0.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Minimal: geometric container with abstract sprout -->
<!-- Container base - single rounded rectangle -->
<rect x="40" y="95" width="120" height="75" rx="8" fill="#2496ED"/>
<!-- Simple horizontal lines -->
<line x1="50" y1="115" x2="150" y2="115" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
<line x1="50" y1="135" x2="150" y2="135" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
<line x1="50" y1="155" x2="150" y2="155" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
<!-- Abstract sprout - two simple leaves -->
<path d="M100 95 L100 55" stroke="#4CAF50" stroke-width="4" stroke-linecap="round"/>
<ellipse cx="85" cy="55" rx="18" ry="10" fill="#4CAF50" transform="rotate(-35 85 55)"/>
<ellipse cx="115" cy="55" rx="18" ry="10" fill="#4CAF50" transform="rotate(35 115 55)"/>
</svg>

After

Width:  |  Height:  |  Size: 913 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Stacked containers with upward growth arrow -->
<!-- Bottom container -->
<rect x="50" y="130" width="100" height="35" rx="4" fill="#2496ED"/>
<!-- Middle container -->
<rect x="60" y="90" width="80" height="35" rx="4" fill="#42A5F5"/>
<!-- Top container -->
<rect x="70" y="50" width="60" height="35" rx="4" fill="#64B5F6"/>
<!-- Growth arrow emerging from top -->
<path d="M100 50 L100 25" stroke="#4CAF50" stroke-width="6" stroke-linecap="round"/>
<polygon points="100,15 90,28 110,28" fill="#4CAF50"/>
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Negative space: container with plant cutout -->
<defs>
<mask id="plantMask">
<rect width="200" height="200" fill="white"/>
<!-- Stem cutout -->
<rect x="96" y="40" width="8" height="55" fill="black"/>
<!-- Leaf cutouts -->
<ellipse cx="82" cy="55" rx="16" ry="9" fill="black" transform="rotate(-40 82 55)"/>
<ellipse cx="118" cy="55" rx="16" ry="9" fill="black" transform="rotate(40 118 55)"/>
<ellipse cx="100" cy="38" rx="10" ry="14" fill="black"/>
</mask>
</defs>
<!-- Container with mask -->
<g mask="url(#plantMask)">
<rect x="35" y="45" width="130" height="120" rx="12" fill="#2496ED"/>
<!-- Ridges -->
<rect x="35" y="70" width="130" height="4" fill="#1976D2"/>
<rect x="35" y="95" width="130" height="4" fill="#1976D2"/>
<rect x="35" y="120" width="130" height="4" fill="#1976D2"/>
<rect x="35" y="145" width="130" height="4" fill="#1976D2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1018 B

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- CF Monogram with container lines -->
<!-- C shape -->
<path d="M45 100
C45 55, 85 35, 115 45
L108 62
C88 55, 62 70, 62 100
C62 130, 88 145, 108 138
L115 155
C85 165, 45 145, 45 100"
fill="#2496ED"/>
<!-- F shape integrated with container lines -->
<rect x="110" y="45" width="50" height="14" rx="2" fill="#2496ED"/>
<rect x="110" y="45" width="14" height="110" rx="2" fill="#2496ED"/>
<rect x="110" y="90" width="40" height="12" rx="2" fill="#2496ED"/>
<!-- Container ridge accents on F -->
<rect x="128" y="63" width="32" height="3" fill="#4CAF50"/>
<rect x="128" y="73" width="32" height="3" fill="#4CAF50"/>
<rect x="128" y="108" width="22" height="3" fill="#4CAF50"/>
<rect x="128" y="118" width="22" height="3" fill="#4CAF50"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Ultra-minimal barn silhouette -->
<!-- Barn shape - single path -->
<path d="M35 170
L35 85
L100 40
L165 85
L165 170
Z"
fill="#2496ED"/>
<!-- Container door accent - simple rectangle grid -->
<g fill="#fff" opacity="0.95">
<rect x="55" y="100" width="35" height="12" rx="2"/>
<rect x="55" y="118" width="35" height="12" rx="2"/>
<rect x="55" y="136" width="35" height="12" rx="2"/>
<rect x="55" y="154" width="35" height="14" rx="2"/>
<rect x="110" y="100" width="35" height="12" rx="2"/>
<rect x="110" y="118" width="35" height="12" rx="2"/>
<rect x="110" y="136" width="35" height="12" rx="2"/>
<rect x="110" y="154" width="35" height="14" rx="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 841 B

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Container grid made of dots - represents distributed containers -->
<g fill="#2496ED">
<!-- Row 1 -->
<circle cx="60" cy="50" r="12"/>
<circle cx="100" cy="50" r="12"/>
<circle cx="140" cy="50" r="12"/>
<!-- Row 2 -->
<circle cx="60" cy="90" r="12"/>
<circle cx="100" cy="90" r="12"/>
<circle cx="140" cy="90" r="12"/>
<!-- Row 3 -->
<circle cx="60" cy="130" r="12"/>
<circle cx="100" cy="130" r="12" fill="#4CAF50"/>
<circle cx="140" cy="130" r="12"/>
</g>
<!-- Connection lines -->
<g stroke="#2496ED" stroke-width="2" opacity="0.4">
<line x1="60" y1="62" x2="60" y2="78"/>
<line x1="100" y1="62" x2="100" y2="78"/>
<line x1="140" y1="62" x2="140" y2="78"/>
<line x1="60" y1="102" x2="60" y2="118"/>
<line x1="100" y1="102" x2="100" y2="118"/>
<line x1="140" y1="102" x2="140" y2="118"/>
<line x1="72" y1="50" x2="88" y2="50"/>
<line x1="112" y1="50" x2="128" y2="50"/>
<line x1="72" y1="90" x2="88" y2="90"/>
<line x1="112" y1="90" x2="128" y2="90"/>
<line x1="72" y1="130" x2="88" y2="130"/>
<line x1="112" y1="130" x2="128" y2="130"/>
</g>
<!-- Sprout from green center -->
<path d="M100 118 L100 155" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
<circle cx="92" cy="158" r="6" fill="#4CAF50"/>
<circle cx="108" cy="158" r="6" fill="#4CAF50"/>
<circle cx="100" cy="165" r="5" fill="#66BB6A"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<!-- Single bold mark: container + leaf combined -->
<!-- Main container shape -->
<rect x="40" y="60" width="120" height="100" rx="10" fill="#2496ED"/>
<!-- Leaf growing from top-right corner -->
<path d="M140 60
Q170 30, 160 55
Q175 25, 145 50
Q165 15, 130 50
L140 60"
fill="#4CAF50"/>
<!-- Minimal internal lines -->
<line x1="55" y1="95" x2="145" y2="95" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
<line x1="55" y1="125" x2="145" y2="125" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,136 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1052 523">
<defs>
<!-- Container blue colors -->
<linearGradient id="containerFront" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#4AA8D8"/>
<stop offset="100%" style="stop-color:#5BB5E0"/>
</linearGradient>
<linearGradient id="containerSide" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3A8AB8"/>
<stop offset="100%" style="stop-color:#4AA8D8"/>
</linearGradient>
<linearGradient id="containerTop" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" style="stop-color:#6BC4E8"/>
<stop offset="100%" style="stop-color:#7DD0F0"/>
</linearGradient>
<linearGradient id="roofGrad" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" style="stop-color:#4AA8D8"/>
<stop offset="100%" style="stop-color:#6BC4E8"/>
</linearGradient>
</defs>
<!-- Green farm field -->
<path d="M526 420 L120 280 L526 200 L932 280 Z" fill="#7CB342"/>
<path d="M526 420 L526 200" stroke="#8BC34A" stroke-width="2"/>
<path d="M526 420 L220 265" stroke="#8BC34A" stroke-width="2"/>
<path d="M526 420 L832 265" stroke="#8BC34A" stroke-width="2"/>
<path d="M526 420 L320 250" stroke="#8BC34A" stroke-width="2"/>
<path d="M526 420 L732 250" stroke="#8BC34A" stroke-width="2"/>
<!-- Field edge -->
<ellipse cx="526" cy="420" rx="410" ry="30" fill="#5D8C2A"/>
<!-- Left container stack -->
<g transform="translate(95, 180)">
<!-- Back container -->
<polygon points="60,40 130,10 130,70 60,100" fill="#3A8AB8"/>
<polygon points="0,70 60,40 60,100 0,130" fill="url(#containerFront)"/>
<polygon points="0,70 60,40 130,70 70,100" fill="url(#containerTop)"/>
<!-- Container ridges -->
<line x1="5" y1="80" x2="55" y2="52" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="95" x2="55" y2="67" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="110" x2="55" y2="82" stroke="#2D7A9E" stroke-width="2"/>
<!-- Front container -->
<polygon points="60,80 130,50 130,110 60,140" fill="#3A8AB8"/>
<polygon points="0,110 60,80 60,140 0,170" fill="url(#containerFront)"/>
<polygon points="0,110 60,80 130,110 70,140" fill="url(#containerTop)"/>
<!-- Container ridges -->
<line x1="5" y1="120" x2="55" y2="92" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="135" x2="55" y2="107" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="150" x2="55" y2="122" stroke="#2D7A9E" stroke-width="2"/>
</g>
<!-- Right container stack -->
<g transform="translate(827, 180)">
<!-- Back container -->
<polygon points="0,10 70,40 70,100 0,70" fill="url(#containerFront)"/>
<polygon points="70,40 130,70 130,130 70,100" fill="#3A8AB8"/>
<polygon points="0,10 70,40 130,10 60,0" fill="url(#containerTop)"/>
<!-- Container ridges -->
<line x1="5" y1="20" x2="65" y2="48" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="35" x2="65" y2="63" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="50" x2="65" y2="78" stroke="#2D7A9E" stroke-width="2"/>
<!-- Front container -->
<polygon points="0,50 70,80 70,140 0,110" fill="url(#containerFront)"/>
<polygon points="70,80 130,110 130,170 70,140" fill="#3A8AB8"/>
<polygon points="0,50 70,80 130,50 60,20" fill="url(#containerTop)"/>
<!-- Container ridges -->
<line x1="5" y1="60" x2="65" y2="88" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="75" x2="65" y2="103" stroke="#2D7A9E" stroke-width="2"/>
<line x1="5" y1="90" x2="65" y2="118" stroke="#2D7A9E" stroke-width="2"/>
</g>
<!-- Dashed connection lines -->
<g stroke="#2D3E50" stroke-width="3" stroke-dasharray="8,6" fill="none">
<path d="M225 320 Q350 280 430 300"/>
<path d="M827 320 Q700 280 620 300"/>
<path d="M225 350 Q300 340 380 360"/>
<path d="M827 350 Q750 340 670 360"/>
</g>
<!-- Central barn/container structure -->
<g transform="translate(400, 85)">
<!-- Base left container -->
<polygon points="0,220 126,155 126,295 0,360" fill="url(#containerFront)"/>
<!-- Container ridges -->
<line x1="10" y1="232" x2="116" y2="172" stroke="#2D7A9E" stroke-width="3"/>
<line x1="10" y1="262" x2="116" y2="202" stroke="#2D7A9E" stroke-width="3"/>
<line x1="10" y1="292" x2="116" y2="232" stroke="#2D7A9E" stroke-width="3"/>
<line x1="10" y1="322" x2="116" y2="262" stroke="#2D7A9E" stroke-width="3"/>
<!-- Base right container -->
<polygon points="126,155 252,220 252,360 126,295" fill="#3A8AB8"/>
<!-- Container ridges -->
<line x1="136" y1="172" x2="242" y2="232" stroke="#2D7A9E" stroke-width="3"/>
<line x1="136" y1="202" x2="242" y2="262" stroke="#2D7A9E" stroke-width="3"/>
<line x1="136" y1="232" x2="242" y2="292" stroke="#2D7A9E" stroke-width="3"/>
<line x1="136" y1="262" x2="242" y2="322" stroke="#2D7A9E" stroke-width="3"/>
<!-- Door opening -->
<polygon points="80,260 126,235 172,260 172,360 126,335 80,360" fill="#1A2530"/>
<!-- Upper left container -->
<polygon points="20,135 126,75 126,155 20,215" fill="url(#containerFront)"/>
<line x1="28" y1="148" x2="118" y2="92" stroke="#2D7A9E" stroke-width="3"/>
<line x1="28" y1="173" x2="118" y2="117" stroke="#2D7A9E" stroke-width="3"/>
<!-- Upper right container -->
<polygon points="126,75 232,135 232,215 126,155" fill="#3A8AB8"/>
<line x1="134" y1="92" x2="224" y2="148" stroke="#2D7A9E" stroke-width="3"/>
<line x1="134" y1="117" x2="224" y2="173" stroke="#2D7A9E" stroke-width="3"/>
<!-- Roof -->
<polygon points="126,0 20,70 20,135 126,75" fill="url(#roofGrad)"/>
<polygon points="126,0 232,70 232,135 126,75" fill="#4AA8D8"/>
<polygon points="126,0 20,70 126,75 232,70" fill="#7DD0F0"/>
<!-- Roof ridges -->
<line x1="30" y1="80" x2="118" y2="35" stroke="#2D7A9E" stroke-width="2"/>
<line x1="30" y1="100" x2="118" y2="55" stroke="#2D7A9E" stroke-width="2"/>
<line x1="134" y1="35" x2="222" y2="80" stroke="#2D7A9E" stroke-width="2"/>
<line x1="134" y1="55" x2="222" y2="100" stroke="#2D7A9E" stroke-width="2"/>
<!-- Weather vane -->
<line x1="126" y1="0" x2="126" y2="-35" stroke="#2D3E50" stroke-width="3"/>
<!-- Crossbar -->
<line x1="100" y1="-25" x2="152" y2="-25" stroke="#2D3E50" stroke-width="3"/>
<!-- Terminal arrows -->
<polygon points="100,-25 108,-20 108,-30" fill="#2D3E50"/>
<polygon points="152,-25 144,-20 144,-30" fill="#2D3E50"/>
<!-- Top arrow -->
<polygon points="126,-35 120,-28 132,-28" fill="#2D3E50"/>
</g>
<!-- COMPOSE FARM text -->
<text x="526" y="500" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="72" font-weight="900" fill="#2D3E50" letter-spacing="4">COMPOSE FARM</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<title>Compose Farm - SSH keyed access</title>
<defs>
<linearGradient id="cfBlue16" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2496ED"/>
<stop offset="100%" stop-color="#1565C0"/>
</linearGradient>
<linearGradient id="cfGreen16" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#66BB6A"/>
<stop offset="100%" stop-color="#2E7D32"/>
</linearGradient>
</defs>
<!-- Container -->
<g>
<rect x="70" y="70" width="115" height="90" rx="10" fill="url(#cfBlue16)"/>
<rect x="70" y="70" width="14" height="90" rx="6" fill="#0D47A1" opacity="0.9"/>
<rect x="171" y="70" width="14" height="90" rx="6" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="88" y="86" width="78" height="12" rx="4"/>
<rect x="88" y="104" width="78" height="12" rx="4"/>
<rect x="88" y="122" width="78" height="12" rx="4"/>
<rect x="88" y="140" width="78" height="12" rx="4"/>
</g>
</g>
<!-- SSH key -->
<g>
<circle cx="38" cy="118" r="18" fill="url(#cfGreen16)"/>
<circle cx="38" cy="118" r="7" fill="#E8F5E9" opacity="0.95"/>
<rect x="54" y="110" width="92" height="16" rx="8" fill="url(#cfGreen16)"/>
<rect x="112" y="126" width="12" height="16" rx="3" fill="#2E7D32"/>
<rect x="130" y="126" width="12" height="24" rx="3" fill="#2E7D32"/>
</g>
<!-- Key insertion hint -->
<path d="M62 118 L78 118" stroke="#E8F5E9" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<title>Compose Farm - reconcile loop</title>
<defs>
<linearGradient id="cfBlue17" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2496ED"/>
<stop offset="100%" stop-color="#1565C0"/>
</linearGradient>
<linearGradient id="cfGreen17" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#66BB6A"/>
<stop offset="100%" stop-color="#2E7D32"/>
</linearGradient>
<marker id="arrowGreen17" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 Z" fill="#2E7D32"/>
</marker>
<marker id="arrowBlue17" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 Z" fill="#1565C0"/>
</marker>
</defs>
<!-- Reconcile arrows -->
<g fill="none" stroke-linecap="round" opacity="0.9">
<path d="M46 112 A70 70 0 0 1 112 40" stroke="url(#cfGreen17)" stroke-width="6" marker-end="url(#arrowGreen17)"/>
<path d="M154 88 A70 70 0 0 1 88 160" stroke="url(#cfBlue17)" stroke-width="6" marker-end="url(#arrowBlue17)"/>
</g>
<!-- Central container -->
<g transform="translate(60,78)">
<rect x="0" y="0" width="80" height="52" rx="8" fill="url(#cfBlue17)"/>
<rect x="0" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
<rect x="70" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="12" y="10" width="56" height="9" rx="3"/>
<rect x="12" y="22" width="56" height="9" rx="3"/>
<rect x="12" y="34" width="56" height="9" rx="3"/>
</g>
</g>
<!-- Apply check -->
<path d="M84 105 L96 117 L118 93" fill="none" stroke="#E8F5E9" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<title>Compose Farm - auto migration</title>
<defs>
<linearGradient id="cfBlue18" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2496ED"/>
<stop offset="100%" stop-color="#1565C0"/>
</linearGradient>
<linearGradient id="cfGreen18" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#66BB6A"/>
<stop offset="100%" stop-color="#2E7D32"/>
</linearGradient>
<marker id="arrow18" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 Z" fill="#2E7D32"/>
</marker>
</defs>
<!-- Hosts -->
<g fill="#334155" opacity="0.9">
<rect x="20" y="55" width="48" height="90" rx="8"/>
<rect x="132" y="55" width="48" height="90" rx="8"/>
</g>
<g fill="#E2E8F0" opacity="0.85">
<rect x="30" y="68" width="28" height="10" rx="3"/>
<rect x="30" y="86" width="28" height="10" rx="3"/>
<rect x="30" y="104" width="28" height="10" rx="3"/>
<circle cx="56" cy="130" r="4"/>
<rect x="142" y="68" width="28" height="10" rx="3"/>
<rect x="142" y="86" width="28" height="10" rx="3"/>
<rect x="142" y="104" width="28" height="10" rx="3"/>
<circle cx="168" cy="130" r="4"/>
</g>
<!-- Migration arrow -->
<path d="M72 100 L128 100" stroke="url(#cfGreen18)" stroke-width="6" stroke-linecap="round" marker-end="url(#arrow18)"/>
<!-- Moving container -->
<g transform="translate(88,86)">
<rect x="0" y="0" width="28" height="20" rx="4" fill="url(#cfBlue18)"/>
<rect x="0" y="0" width="5" height="20" rx="3" fill="#0D47A1" opacity="0.9"/>
<rect x="23" y="0" width="5" height="20" rx="3" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="6" y="5" width="16" height="4" rx="2"/>
<rect x="6" y="11" width="16" height="4" rx="2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<title>Compose Farm - multi-host compose</title>
<defs>
<linearGradient id="cfBlue15" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2496ED"/>
<stop offset="100%" stop-color="#1565C0"/>
</linearGradient>
<linearGradient id="cfGreen15" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#81C784"/>
<stop offset="100%" stop-color="#2E7D32"/>
</linearGradient>
</defs>
<!-- Field -->
<polygon points="16,170 184,170 150,108 50,108" fill="url(#cfGreen15)" opacity="0.35"/>
<g stroke="#2E7D32" stroke-width="2" opacity="0.22" stroke-linecap="round">
<line x1="34" y1="170" x2="62" y2="108"/>
<line x1="70" y1="170" x2="86" y2="108"/>
<line x1="100" y1="170" x2="100" y2="108"/>
<line x1="130" y1="170" x2="114" y2="108"/>
<line x1="166" y1="170" x2="138" y2="108"/>
</g>
<!-- SSH paths -->
<g stroke="#334155" stroke-width="3" stroke-dasharray="4 6" opacity="0.75" fill="none" stroke-linecap="round">
<path d="M100 132 C82 118 68 114 46 118"/>
<path d="M100 132 C118 118 132 114 154 118"/>
</g>
<g fill="#334155" opacity="0.75">
<circle cx="100" cy="132" r="3"/>
<circle cx="46" cy="118" r="3"/>
<circle cx="154" cy="118" r="3"/>
</g>
<!-- Remote container: left -->
<g transform="translate(18,118)">
<rect x="0" y="0" width="56" height="34" rx="5" fill="url(#cfBlue15)"/>
<rect x="0" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
<rect x="49" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="9" y="7" width="38" height="6" rx="2"/>
<rect x="9" y="15" width="38" height="6" rx="2"/>
<rect x="9" y="23" width="38" height="6" rx="2"/>
</g>
</g>
<!-- Remote container: right -->
<g transform="translate(126,118)">
<rect x="0" y="0" width="56" height="34" rx="5" fill="url(#cfBlue15)"/>
<rect x="0" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
<rect x="49" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="9" y="7" width="38" height="6" rx="2"/>
<rect x="9" y="15" width="38" height="6" rx="2"/>
<rect x="9" y="23" width="38" height="6" rx="2"/>
</g>
</g>
<!-- Central container with prompt -->
<g transform="translate(58,80)">
<rect x="0" y="0" width="84" height="52" rx="7" fill="url(#cfBlue15)"/>
<rect x="0" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
<rect x="74" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
<g fill="#1A7AC7" opacity="0.9">
<rect x="12" y="10" width="60" height="9" rx="3"/>
<rect x="12" y="22" width="60" height="9" rx="3"/>
<rect x="12" y="34" width="60" height="9" rx="3"/>
</g>
<!-- Prompt: >_ -->
<g stroke="#F8FAFC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.95">
<polyline points="18,18 30,26 18,34"/>
<line x1="36" y1="34" x2="58" y2="34"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
import contextlib
import os
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, TypeVar
@@ -69,21 +68,6 @@ ServiceOption = Annotated[
_MISSING_PATH_PREVIEW_LIMIT = 2
_STATS_PREVIEW_LIMIT = 3 # Max number of pending migrations to show by name
# Environment variable to identify the web stack (for self-update ordering)
CF_WEB_STACK = os.environ.get("CF_WEB_STACK", "")
def sort_web_stack_last(stacks: list[str]) -> list[str]:
"""Move the web stack to the end of the list.
When updating all stacks, the web UI stack (compose-farm) should be updated
last. Otherwise, the container restarts mid-process and cancels remaining
updates. The CF_WEB_STACK env var identifies the web stack.
"""
if CF_WEB_STACK and CF_WEB_STACK in stacks:
return [s for s in stacks if s != CF_WEB_STACK] + [CF_WEB_STACK]
return stacks
def format_host(host: str | list[str]) -> str:
"""Format a host value for display."""

View File

@@ -23,7 +23,6 @@ from compose_farm.cli.common import (
maybe_regenerate_traefik,
report_results,
run_async,
sort_web_stack_last,
validate_host_for_stack,
validate_stacks,
)
@@ -172,8 +171,6 @@ def restart(
raw = True
results = run_async(run_on_stacks(cfg, stack_list, f"restart {service}", raw=raw))
else:
# Sort web stack last to avoid self-restart canceling remaining restarts
stack_list = sort_web_stack_last(stack_list)
raw = len(stack_list) == 1
results = run_async(run_sequential_on_stacks(cfg, stack_list, ["down", "up -d"], raw=raw))
maybe_regenerate_traefik(cfg, results)
@@ -187,36 +184,17 @@ def update(
service: ServiceOption = None,
config: ConfigOption = None,
) -> None:
"""Update stacks (pull + build + down + up). With --service, updates just that service."""
"""Update stacks. Only recreates containers if images changed."""
stack_list, cfg = get_stacks(stacks or [], all_stacks, config)
if service:
if len(stack_list) != 1:
print_error("--service requires exactly one stack")
raise typer.Exit(1)
# For service-level update: pull + build + stop + up (stop instead of down)
raw = True
results = run_async(
run_sequential_on_stacks(
cfg,
stack_list,
[
f"pull --ignore-buildable {service}",
f"build {service}",
f"stop {service}",
f"up -d {service}",
],
raw=raw,
)
)
cmd = f"up -d --pull always --build {service}"
else:
# Sort web stack last to avoid self-restart canceling remaining updates
stack_list = sort_web_stack_last(stack_list)
raw = len(stack_list) == 1
results = run_async(
run_sequential_on_stacks(
cfg, stack_list, ["pull --ignore-buildable", "build", "down", "up -d"], raw=raw
)
)
cmd = "up -d --pull always --build"
raw = len(stack_list) == 1
results = run_async(run_on_stacks(cfg, stack_list, cmd, raw=raw))
maybe_regenerate_traefik(cfg, results)
report_results(results)
@@ -333,11 +311,6 @@ def apply( # noqa: C901, PLR0912, PLR0915 (multi-phase reconciliation needs the
console.print(f"\n{MSG_DRY_RUN}")
return
# Sort web stack last in each phase to avoid self-restart canceling remaining work
migrations = sort_web_stack_last(migrations)
missing = sort_web_stack_last(missing)
to_refresh = sort_web_stack_last(to_refresh)
# Execute changes
console.print()
all_results = []

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import os
import uuid
from typing import TYPE_CHECKING, Any
@@ -14,6 +15,9 @@ if TYPE_CHECKING:
from compose_farm.web.deps import get_config
from compose_farm.web.streaming import run_cli_streaming, run_compose_streaming, tasks
# Environment variable to identify the web stack (for exclusion from bulk updates)
CF_WEB_STACK = os.environ.get("CF_WEB_STACK", "")
router = APIRouter(tags=["actions"])
# Store task references to prevent garbage collection
@@ -96,7 +100,15 @@ async def pull_all() -> dict[str, Any]:
@router.post("/update-all")
async def update_all() -> dict[str, Any]:
"""Update all stacks (pull + build + down + up)."""
"""Update all stacks, excluding the web stack. Only recreates if images changed.
The web stack is excluded to prevent the UI from shutting down mid-operation.
Use 'cf update <web-stack>' manually to update the web UI.
"""
config = get_config()
task_id = _start_task(lambda tid: run_cli_streaming(config, ["update", "--all"], tid))
return {"task_id": task_id, "command": "update --all"}
# Get all stacks except the web stack to avoid self-shutdown
stacks = [s for s in config.stacks if s != CF_WEB_STACK]
if not stacks:
return {"task_id": "", "command": "update (no stacks)", "skipped": True}
task_id = _start_task(lambda tid: run_cli_streaming(config, ["update", *stacks], tid))
return {"task_id": task_id, "command": f"update {' '.join(stacks)}"}

View File

@@ -608,7 +608,7 @@ function playFabIntro() {
cmd('action', 'Apply', 'Make reality match config', dashboardAction('apply'), icons.check),
cmd('action', 'Refresh', 'Update state from reality', dashboardAction('refresh'), icons.refresh_cw),
cmd('action', 'Pull All', 'Pull latest images for all stacks', dashboardAction('pull-all'), icons.cloud_download),
cmd('action', 'Update All', 'Update all stacks', dashboardAction('update-all'), icons.refresh_cw),
cmd('action', 'Update All', 'Update all stacks except web', dashboardAction('update-all'), icons.refresh_cw),
cmd('app', 'Theme', 'Change color theme', openThemePicker, icons.palette),
cmd('app', 'Dashboard', 'Go to dashboard', nav('/'), icons.home),
cmd('app', 'Live Stats', 'View all containers across hosts', nav('/live-stats'), icons.box),

View File

@@ -18,7 +18,7 @@
{{ action_btn("Apply", "/api/apply", "primary", "Make reality match config", check()) }}
{{ action_btn("Refresh", "/api/refresh", "outline", "Update state from reality", refresh_cw()) }}
{{ action_btn("Pull All", "/api/pull-all", "outline", "Pull latest images for all stacks", cloud_download()) }}
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks (pull + build + down + up)", rotate_cw()) }}
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks except web (only restarts if changed)", rotate_cw()) }}
<div class="tooltip" data-tip="Save compose-farm.yaml config file"><button id="save-config-btn" class="btn btn-outline">{{ save() }} Save Config</button></div>
</div>

View File

@@ -23,7 +23,7 @@
{{ action_btn("Up", "/api/stack/" ~ name ~ "/up", "primary", "Start stack (docker compose up -d)", play()) }}
{{ action_btn("Down", "/api/stack/" ~ name ~ "/down", "outline", "Stop stack (docker compose down)", square()) }}
{{ action_btn("Restart", "/api/stack/" ~ name ~ "/restart", "secondary", "Restart stack (down + up)", rotate_cw()) }}
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (pull + build + down + up)", download()) }}
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (only restarts if changed)", download()) }}
<div class="divider divider-horizontal mx-0"></div>

View File

@@ -437,71 +437,3 @@ class TestDownOrphaned:
)
assert exc_info.value.exit_code == 1
class TestSortWebStackLast:
"""Tests for the sort_web_stack_last helper."""
def test_no_web_stack_env(self) -> None:
"""When CF_WEB_STACK is not set, list is unchanged."""
from compose_farm.cli import common
original = common.CF_WEB_STACK
try:
common.CF_WEB_STACK = ""
stacks = ["a", "b", "c"]
result = common.sort_web_stack_last(stacks)
assert result == ["a", "b", "c"]
finally:
common.CF_WEB_STACK = original
def test_web_stack_not_in_list(self) -> None:
"""When web stack is not in list, list is unchanged."""
from compose_farm.cli import common
original = common.CF_WEB_STACK
try:
common.CF_WEB_STACK = "webstack"
stacks = ["a", "b", "c"]
result = common.sort_web_stack_last(stacks)
assert result == ["a", "b", "c"]
finally:
common.CF_WEB_STACK = original
def test_web_stack_moved_to_end(self) -> None:
"""When web stack is in list, it's moved to the end."""
from compose_farm.cli import common
original = common.CF_WEB_STACK
try:
common.CF_WEB_STACK = "webstack"
stacks = ["a", "webstack", "b", "c"]
result = common.sort_web_stack_last(stacks)
assert result == ["a", "b", "c", "webstack"]
finally:
common.CF_WEB_STACK = original
def test_web_stack_already_last(self) -> None:
"""When web stack is already last, list is unchanged."""
from compose_farm.cli import common
original = common.CF_WEB_STACK
try:
common.CF_WEB_STACK = "webstack"
stacks = ["a", "b", "webstack"]
result = common.sort_web_stack_last(stacks)
assert result == ["a", "b", "webstack"]
finally:
common.CF_WEB_STACK = original
def test_empty_list(self) -> None:
"""Empty list returns empty list."""
from compose_farm.cli import common
original = common.CF_WEB_STACK
try:
common.CF_WEB_STACK = "webstack"
result = common.sort_web_stack_last([])
assert result == []
finally:
common.CF_WEB_STACK = original

View File

@@ -98,19 +98,18 @@ class TestMigrationCommands:
class TestUpdateCommandSequence:
"""Tests for update command sequence."""
def test_update_command_sequence_includes_build(self) -> None:
"""Update command should use pull --ignore-buildable and build."""
def test_update_command_uses_pull_always_and_build(self) -> None:
"""Update command should use --pull always --build flags."""
# This is a static check of the command sequence in lifecycle.py
# The actual command sequence is defined in the update function
source = inspect.getsource(lifecycle.update)
# Verify the command sequence includes pull --ignore-buildable
assert "pull --ignore-buildable" in source
# Verify build is included
assert '"build"' in source or "'build'" in source
# Verify the sequence is pull, build, down, up
assert "down" in source
# Verify the command uses --pull always (only recreates if image changed)
assert "--pull always" in source
# Verify --build is included for buildable services
assert "--build" in source
# Verify up -d is used
assert "up -d" in source