mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
725 lines
18 KiB
Markdown
725 lines
18 KiB
Markdown
# Development Workflow for ATCR
|
|
|
|
## The Problem
|
|
|
|
**Current development cycle with Docker:**
|
|
1. Edit CSS, JS, template, or Go file
|
|
2. Run `docker compose build` (rebuilds entire image)
|
|
3. Run `docker compose up` (restart container)
|
|
4. Wait **2-3 minutes** for changes to appear
|
|
5. Test, find issue, repeat...
|
|
|
|
**Why it's slow:**
|
|
- All assets embedded via `embed.FS` at compile time
|
|
- Multi-stage Docker build compiles everything from scratch
|
|
- No development mode exists
|
|
- Final image uses `scratch` base (no tools, no hot reload)
|
|
|
|
## The Solution
|
|
|
|
**Development setup combining:**
|
|
1. **Dockerfile.devel** - Development-focused container (golang base, not scratch)
|
|
2. **Volume mounts** - Live code editing (changes appear instantly in container)
|
|
3. **DirFS** - Skip embed, read templates/CSS/JS from filesystem
|
|
4. **Air** - Auto-rebuild on Go code changes
|
|
|
|
**Results:**
|
|
- CSS/JS/Template changes: **Instant** (0 seconds, just refresh browser)
|
|
- Go code changes: **2-5 seconds** (vs 2-3 minutes)
|
|
- Production builds: **Unchanged** (still optimized with embed.FS)
|
|
|
|
## How It Works
|
|
|
|
### Architecture Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Your Editor (VSCode, etc) │
|
|
│ Edit: style.css, app.js, *.html, *.go files │
|
|
└─────────────────┬───────────────────────────────────┘
|
|
│ (files saved to disk)
|
|
▼
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Volume Mount (docker-compose.dev.yml) │
|
|
│ volumes: │
|
|
│ - .:/app (entire codebase mounted) │
|
|
└─────────────────┬───────────────────────────────────┘
|
|
│ (changes appear instantly in container)
|
|
▼
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Container (golang:1.25.2 base, has all tools) │
|
|
│ │
|
|
│ ┌──────────────────────────────────────┐ │
|
|
│ │ Air (hot reload tool) │ │
|
|
│ │ Watches: *.go, *.html, *.css, *.js │ │
|
|
│ │ │ │
|
|
│ │ On change: │ │
|
|
│ │ - *.go → rebuild binary (2-5s) │ │
|
|
│ │ - templates/css/js → restart only │ │
|
|
│ └──────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────────────────────────────┐ │
|
|
│ │ ATCR AppView (ATCR_DEV_MODE=true) │ │
|
|
│ │ │ │
|
|
│ │ ui.go checks DEV_MODE: │ │
|
|
│ │ if DEV_MODE: │ │
|
|
│ │ templatesFS = os.DirFS("...") │ │
|
|
│ │ publicFS = os.DirFS("...") │ │
|
|
│ │ else: │ │
|
|
│ │ use embed.FS (production) │ │
|
|
│ │ │ │
|
|
│ │ Result: Reads from mounted files │ │
|
|
│ └──────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Change Scenarios
|
|
|
|
#### Scenario 1: Edit CSS/JS/Templates
|
|
```
|
|
1. Edit pkg/appview/public/css/style.css in VSCode
|
|
2. Save file
|
|
3. Change appears in container via volume mount (instant)
|
|
4. App uses os.DirFS → reads new file from disk (instant)
|
|
5. Refresh browser → see changes
|
|
```
|
|
**Time:** **Instant** (0 seconds)
|
|
**No rebuild, no restart!**
|
|
|
|
#### Scenario 2: Edit Go Code
|
|
```
|
|
1. Edit pkg/appview/handlers/home.go
|
|
2. Save file
|
|
3. Air detects .go file change
|
|
4. Air runs: go build -o ./tmp/atcr-appview ./cmd/appview
|
|
5. Air kills old process and starts new binary
|
|
6. App runs with new code
|
|
```
|
|
**Time:** **2-5 seconds**
|
|
**Fast incremental build!**
|
|
|
|
## Implementation
|
|
|
|
### Step 1: Create Dockerfile.devel
|
|
|
|
Create `Dockerfile.devel` in project root:
|
|
|
|
```dockerfile
|
|
# Development Dockerfile with hot reload support
|
|
FROM golang:1.25.2-trixie
|
|
|
|
# Install Air for hot reload
|
|
RUN go install github.com/cosmtrek/air@latest
|
|
|
|
# Install SQLite (required for CGO in ATCR)
|
|
RUN apt-get update && apt-get install -y \
|
|
sqlite3 \
|
|
libsqlite3-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy dependency files and download (cached layer)
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
# Note: Source code comes from volume mount
|
|
# (no COPY . . needed - that's the whole point!)
|
|
|
|
# Air will handle building and running
|
|
CMD ["air", "-c", ".air.toml"]
|
|
```
|
|
|
|
### Step 2: Create docker-compose.dev.yml
|
|
|
|
Create `docker-compose.dev.yml` in project root:
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
atcr-appview:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.devel
|
|
volumes:
|
|
# Mount entire codebase (live editing)
|
|
- .:/app
|
|
# Cache Go modules (faster rebuilds)
|
|
- go-cache:/go/pkg/mod
|
|
# Persist SQLite database
|
|
- atcr-ui-dev:/var/lib/atcr
|
|
environment:
|
|
# Enable development mode (uses os.DirFS)
|
|
ATCR_DEV_MODE: "true"
|
|
|
|
# AppView configuration
|
|
ATCR_HTTP_ADDR: ":5000"
|
|
ATCR_BASE_URL: "http://localhost:5000"
|
|
ATCR_DEFAULT_HOLD_DID: "did:web:hold01.atcr.io"
|
|
|
|
# Database
|
|
ATCR_UI_DATABASE_PATH: "/var/lib/atcr/ui.db"
|
|
|
|
# Auth
|
|
ATCR_AUTH_KEY_PATH: "/var/lib/atcr/auth/private-key.pem"
|
|
|
|
# Jetstream (optional)
|
|
# JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe"
|
|
# ATCR_BACKFILL_ENABLED: "false"
|
|
ports:
|
|
- "5000:5000"
|
|
networks:
|
|
- atcr-dev
|
|
|
|
# Add other services as needed (postgres, hold, etc)
|
|
# atcr-hold:
|
|
# ...
|
|
|
|
networks:
|
|
atcr-dev:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
go-cache:
|
|
atcr-ui-dev:
|
|
```
|
|
|
|
### Step 3: Create .air.toml
|
|
|
|
Create `.air.toml` in project root:
|
|
|
|
```toml
|
|
# Air configuration for hot reload
|
|
# https://github.com/cosmtrek/air
|
|
|
|
root = "."
|
|
testdata_dir = "testdata"
|
|
tmp_dir = "tmp"
|
|
|
|
[build]
|
|
# Arguments to pass to binary (AppView needs "serve")
|
|
args_bin = ["serve"]
|
|
|
|
# Where to output the built binary
|
|
bin = "./tmp/atcr-appview"
|
|
|
|
# Build command
|
|
cmd = "go build -o ./tmp/atcr-appview ./cmd/appview"
|
|
|
|
# Delay before rebuilding (ms) - debounce rapid saves
|
|
delay = 1000
|
|
|
|
# Directories to exclude from watching
|
|
exclude_dir = [
|
|
"tmp",
|
|
"vendor",
|
|
"bin",
|
|
".git",
|
|
"node_modules",
|
|
"testdata"
|
|
]
|
|
|
|
# Files to exclude from watching
|
|
exclude_file = []
|
|
|
|
# Regex patterns to exclude
|
|
exclude_regex = ["_test\\.go"]
|
|
|
|
# Don't rebuild if file content unchanged
|
|
exclude_unchanged = false
|
|
|
|
# Follow symlinks
|
|
follow_symlink = false
|
|
|
|
# Full command to run (leave empty to use cmd + bin)
|
|
full_bin = ""
|
|
|
|
# Directories to include (empty = all)
|
|
include_dir = []
|
|
|
|
# File extensions to watch
|
|
include_ext = ["go", "html", "css", "js"]
|
|
|
|
# Specific files to watch
|
|
include_file = []
|
|
|
|
# Delay before killing old process (s)
|
|
kill_delay = "0s"
|
|
|
|
# Log file for build errors
|
|
log = "build-errors.log"
|
|
|
|
# Use polling instead of fsnotify (for Docker/VM)
|
|
poll = false
|
|
poll_interval = 0
|
|
|
|
# Rerun binary if it exits
|
|
rerun = false
|
|
rerun_delay = 500
|
|
|
|
# Send interrupt signal instead of kill
|
|
send_interrupt = false
|
|
|
|
# Stop on build error
|
|
stop_on_error = false
|
|
|
|
[color]
|
|
# Colorize output
|
|
app = ""
|
|
build = "yellow"
|
|
main = "magenta"
|
|
runner = "green"
|
|
watcher = "cyan"
|
|
|
|
[log]
|
|
# Show only app logs (not build logs)
|
|
main_only = false
|
|
|
|
# Add timestamp to logs
|
|
time = false
|
|
|
|
[misc]
|
|
# Clean tmp directory on exit
|
|
clean_on_exit = false
|
|
|
|
[screen]
|
|
# Clear screen on rebuild
|
|
clear_on_rebuild = false
|
|
|
|
# Keep scrollback
|
|
keep_scroll = true
|
|
```
|
|
|
|
### Step 4: Modify pkg/appview/ui.go
|
|
|
|
Add conditional filesystem loading to `pkg/appview/ui.go`:
|
|
|
|
```go
|
|
package appview
|
|
|
|
import (
|
|
"embed"
|
|
"html/template"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
)
|
|
|
|
// Embedded assets (used in production)
|
|
//go:embed templates/**/*.html
|
|
var embeddedTemplatesFS embed.FS
|
|
|
|
//go:embed static
|
|
var embeddedpublicFS embed.FS
|
|
|
|
// Actual filesystems used at runtime (conditional)
|
|
var templatesFS fs.FS
|
|
var publicFS fs.FS
|
|
|
|
func init() {
|
|
// Development mode: read from filesystem for instant updates
|
|
if os.Getenv("ATCR_DEV_MODE") == "true" {
|
|
log.Println("🔧 DEV MODE: Using filesystem for templates and static assets")
|
|
templatesFS = os.DirFS("pkg/appview/templates")
|
|
publicFS = os.DirFS("pkg/appview/static")
|
|
} else {
|
|
// Production mode: use embedded assets
|
|
log.Println("📦 PRODUCTION MODE: Using embedded assets")
|
|
templatesFS = embeddedTemplatesFS
|
|
publicFS = embeddedpublicFS
|
|
}
|
|
}
|
|
|
|
// Templates returns parsed HTML templates
|
|
func Templates() *template.Template {
|
|
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse templates: %v", err)
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
// StaticHandler returns a handler for static files
|
|
func StaticHandler() http.Handler {
|
|
sub, err := fs.Sub(publicFS, "static")
|
|
if err != nil {
|
|
log.Fatalf("Failed to create static sub-filesystem: %v", err)
|
|
}
|
|
return http.FileServer(http.FS(sub))
|
|
}
|
|
```
|
|
|
|
**Important:** Update the `Templates()` function to NOT cache templates in dev mode:
|
|
|
|
```go
|
|
// Templates returns parsed HTML templates
|
|
func Templates() *template.Template {
|
|
// In dev mode, reparse templates on every request (instant updates)
|
|
// In production, this could be cached
|
|
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse templates: %v", err)
|
|
}
|
|
return tmpl
|
|
}
|
|
```
|
|
|
|
If you're caching templates, wrap it with a dev mode check:
|
|
|
|
```go
|
|
var templateCache *template.Template
|
|
|
|
func Templates() *template.Template {
|
|
// Development: reparse every time (instant updates)
|
|
if os.Getenv("ATCR_DEV_MODE") == "true" {
|
|
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
|
if err != nil {
|
|
log.Printf("Template parse error: %v", err)
|
|
return template.New("error")
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
// Production: use cached templates
|
|
if templateCache == nil {
|
|
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse templates: %v", err)
|
|
}
|
|
templateCache = tmpl
|
|
}
|
|
return templateCache
|
|
}
|
|
```
|
|
|
|
### Step 5: Add to .gitignore
|
|
|
|
Add Air's temporary directory to `.gitignore`:
|
|
|
|
```
|
|
# Air hot reload
|
|
tmp/
|
|
build-errors.log
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Starting Development Environment
|
|
|
|
```bash
|
|
# Build and start dev container
|
|
docker compose -f docker-compose.dev.yml up --build
|
|
|
|
# Or run in background
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
|
|
# View logs
|
|
docker compose -f docker-compose.dev.yml logs -f atcr-appview
|
|
```
|
|
|
|
You should see Air starting:
|
|
|
|
```
|
|
atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets
|
|
atcr-appview |
|
|
atcr-appview | __ _ ___
|
|
atcr-appview | / /\ | | | |_)
|
|
atcr-appview | /_/--\ |_| |_| \_ , built with Go
|
|
atcr-appview |
|
|
atcr-appview | watching .
|
|
atcr-appview | !exclude tmp
|
|
atcr-appview | building...
|
|
atcr-appview | running...
|
|
```
|
|
|
|
### Development Workflow
|
|
|
|
#### 1. Edit Templates/CSS/JS (Instant Updates)
|
|
|
|
```bash
|
|
# Edit any template, CSS, or JS file
|
|
vim pkg/appview/templates/pages/home.html
|
|
vim pkg/appview/public/css/style.css
|
|
vim pkg/appview/public/js/app.js
|
|
|
|
# Save file → changes appear instantly
|
|
# Just refresh browser (Cmd+R / Ctrl+R)
|
|
```
|
|
|
|
**No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed.
|
|
|
|
#### 2. Edit Go Code (Fast Rebuild)
|
|
|
|
```bash
|
|
# Edit any Go file
|
|
vim pkg/appview/handlers/home.go
|
|
|
|
# Save file → Air detects change
|
|
# Air output shows:
|
|
# building...
|
|
# build successful in 2.3s
|
|
# restarting...
|
|
|
|
# Refresh browser to see changes
|
|
```
|
|
|
|
**2-5 second rebuild** instead of 2-3 minutes!
|
|
|
|
### Stopping Development Environment
|
|
|
|
```bash
|
|
# Stop containers
|
|
docker compose -f docker-compose.dev.yml down
|
|
|
|
# Stop and remove volumes (fresh start)
|
|
docker compose -f docker-compose.dev.yml down -v
|
|
```
|
|
|
|
## Production Builds
|
|
|
|
**Production builds are completely unchanged:**
|
|
|
|
```bash
|
|
# Production uses normal Dockerfile (embed.FS, scratch base)
|
|
docker compose build
|
|
|
|
# Or specific service
|
|
docker compose build atcr-appview
|
|
|
|
# Run production
|
|
docker compose up
|
|
```
|
|
|
|
**Why it works:**
|
|
- Production doesn't set `ATCR_DEV_MODE=true`
|
|
- `ui.go` defaults to embedded assets when env var is unset
|
|
- Production Dockerfile still uses multi-stage build to scratch
|
|
- No development dependencies in production image
|
|
|
|
## Comparison
|
|
|
|
| Change Type | Before (docker compose) | After (dev setup) | Improvement |
|
|
|-------------|------------------------|-------------------|-------------|
|
|
| Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
|
| Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
|
| Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
|
| Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster |
|
|
| Production Build | Same | **Same** | No change |
|
|
|
|
## Advanced: Local Development (No Docker)
|
|
|
|
For even faster development, run locally without Docker:
|
|
|
|
```bash
|
|
# Set environment variables
|
|
export ATCR_DEV_MODE=true
|
|
export ATCR_HTTP_ADDR=:5000
|
|
export ATCR_BASE_URL=http://localhost:5000
|
|
export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io
|
|
export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db
|
|
export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem
|
|
|
|
# Or use .env file
|
|
source .env.appview
|
|
|
|
# Run with Air
|
|
air -c .air.toml
|
|
|
|
# Or run directly (no hot reload)
|
|
go run ./cmd/appview serve
|
|
```
|
|
|
|
**Advantages:**
|
|
- Even faster (no Docker overhead)
|
|
- Native debugging with delve
|
|
- Direct filesystem access
|
|
- Full IDE integration
|
|
|
|
**Disadvantages:**
|
|
- Need to manage dependencies locally (SQLite, etc)
|
|
- May differ from production environment
|
|
|
|
## Troubleshooting
|
|
|
|
### Air Not Rebuilding
|
|
|
|
**Problem:** Air doesn't detect changes
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Check if Air is actually running
|
|
docker compose -f docker-compose.dev.yml logs atcr-appview
|
|
|
|
# Check .air.toml include_ext includes your file type
|
|
# Default: ["go", "html", "css", "js"]
|
|
|
|
# Restart container
|
|
docker compose -f docker-compose.dev.yml restart atcr-appview
|
|
```
|
|
|
|
### Templates Not Updating
|
|
|
|
**Problem:** Template changes don't appear
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Check ATCR_DEV_MODE is set
|
|
docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE
|
|
|
|
# Should output: ATCR_DEV_MODE=true
|
|
|
|
# Check templates aren't cached (see Step 4 above)
|
|
# Templates() should reparse in dev mode
|
|
```
|
|
|
|
### Go Build Failing
|
|
|
|
**Problem:** Air shows build errors
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Check build logs
|
|
docker compose -f docker-compose.dev.yml logs atcr-appview
|
|
|
|
# Or check build-errors.log in container
|
|
docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log
|
|
|
|
# Fix the Go error, save file, Air will retry
|
|
```
|
|
|
|
### Volume Mount Not Working
|
|
|
|
**Problem:** Changes don't appear in container
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Verify volume mount
|
|
docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app
|
|
|
|
# Should show your source files
|
|
|
|
# On Windows/Mac, check Docker Desktop file sharing settings
|
|
# Settings → Resources → File Sharing → add project directory
|
|
```
|
|
|
|
### Permission Errors
|
|
|
|
**Problem:** Cannot write to /var/lib/atcr
|
|
|
|
**Solution:**
|
|
```bash
|
|
# In Dockerfile.devel, add:
|
|
RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr
|
|
|
|
# Or use named volumes (already in docker-compose.dev.yml)
|
|
volumes:
|
|
- atcr-ui-dev:/var/lib/atcr
|
|
```
|
|
|
|
### Slow Builds Even with Air
|
|
|
|
**Problem:** Air rebuilds slowly
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Use Go module cache volume (already in docker-compose.dev.yml)
|
|
volumes:
|
|
- go-cache:/go/pkg/mod
|
|
|
|
# Increase Air delay to debounce rapid saves
|
|
# In .air.toml:
|
|
delay = 2000 # 2 seconds
|
|
|
|
# Or check if CGO is slowing builds
|
|
# AppView needs CGO for SQLite, but you can try:
|
|
CGO_ENABLED=0 go build # (won't work for ATCR, but good to know)
|
|
```
|
|
|
|
## Tips & Tricks
|
|
|
|
### Browser Auto-Reload (LiveReload)
|
|
|
|
Add LiveReload for automatic browser refresh:
|
|
|
|
```bash
|
|
# Install browser extension
|
|
# Chrome: https://chrome.google.com/webstore/detail/livereload
|
|
# Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/
|
|
|
|
# Add livereload to .air.toml (future Air feature)
|
|
# Or use a separate tool like browsersync
|
|
```
|
|
|
|
### Database Resets
|
|
|
|
Development database is in a named volume:
|
|
|
|
```bash
|
|
# Reset database (fresh start)
|
|
docker compose -f docker-compose.dev.yml down -v
|
|
docker compose -f docker-compose.dev.yml up
|
|
|
|
# Or delete specific volume
|
|
docker volume rm atcr_atcr-ui-dev
|
|
```
|
|
|
|
### Multiple Environments
|
|
|
|
Run dev and production side-by-side:
|
|
|
|
```bash
|
|
# Development on port 5000
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
|
|
# Production on port 5001
|
|
docker compose up -d
|
|
|
|
# Now you can compare behavior
|
|
```
|
|
|
|
### Debugging with Delve
|
|
|
|
Add delve to Dockerfile.devel:
|
|
|
|
```dockerfile
|
|
RUN go install github.com/go-delve/delve/cmd/dlv@latest
|
|
|
|
# Change CMD to use delve
|
|
CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"]
|
|
```
|
|
|
|
Then connect with VSCode or GoLand.
|
|
|
|
## Summary
|
|
|
|
**Development Setup (One-Time):**
|
|
1. Create `Dockerfile.devel`
|
|
2. Create `docker-compose.dev.yml`
|
|
3. Create `.air.toml`
|
|
4. Modify `pkg/appview/ui.go` for conditional DirFS
|
|
5. Add `tmp/` to `.gitignore`
|
|
|
|
**Daily Development:**
|
|
```bash
|
|
# Start
|
|
docker compose -f docker-compose.dev.yml up
|
|
|
|
# Edit files in your editor
|
|
# Changes appear instantly (CSS/JS/templates)
|
|
# Or in 2-5 seconds (Go code)
|
|
|
|
# Stop
|
|
docker compose -f docker-compose.dev.yml down
|
|
```
|
|
|
|
**Production (Unchanged):**
|
|
```bash
|
|
docker compose build
|
|
docker compose up
|
|
```
|
|
|
|
**Result:** 100x faster development iteration! 🚀
|