mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-03 14:13:26 +00:00
docs: improve Web UI workflow demo with comprehensive showcase (#78)
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7fd5d0a55478b2228d3374fa7153f0a573f75f949b8dc373b467ef8313faf8fd
|
||||
size 1722539
|
||||
oid sha256:dac5660cfe6574857ec055fac7822f25b7c5fcb10a836b19c86142515e2fbf75
|
||||
size 1816075
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00fcd406dc567e778c6fac8cd6096b1ca28038d2da07a7380ffe6ae6baeab041
|
||||
size 1505468
|
||||
oid sha256:d4efec8ef5a99f2cb31d55cd71cdbf0bb8dd0cd6281571886b7c1f8b41c3f9da
|
||||
size 1660764
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:52c5b293bd76f310a827778f26e64b4ef43a5d148b7b487e77ca89331bd7a19e
|
||||
size 3348205
|
||||
oid sha256:9348dd36e79192344476d61fbbffdb122a96ecc5829fbece1818590cfc521521
|
||||
size 3373003
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62f31d1bee465dea7b75933266679e21d77af95b80bf7138231f706f0d2534d3
|
||||
size 2750918
|
||||
oid sha256:bebbf8151434ba37bf5e46566a4e8b57812944281926f579d056bdc835ca26aa
|
||||
size 2729799
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4e6f2a1e932fc30801ec47dd6644fbc1e83878e33f80684952c938d8c1306274
|
||||
size 1491217
|
||||
oid sha256:3712afff6fcde00eb951264bb24d4301deb085d082b4e95ed4c1893a571938ee
|
||||
size 1528294
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a9591049f878763ba1bd715746e8e4f285ed2632d035a14023e5bb41f7489d7
|
||||
size 1201920
|
||||
oid sha256:0b218d400836a50661c9cdcce2d2b1e285cc5fe592cb42f58aae41f3e7d60684
|
||||
size 1327413
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:95e1a49cbe1b383ae82fcdbb2d5e25e100aa66f805f2974403b8ab45585dae40
|
||||
size 3582989
|
||||
oid sha256:6a232ddc1b9ddd9bf6b5d99c05153e1094be56f1952f02636ca498eb7484e096
|
||||
size 3808675
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db3ef4e9336bf3d7840ccb68c1daf87adaad97809171c6460f6186a4c29248f3
|
||||
size 3111066
|
||||
oid sha256:5a7c9f5f6d47074a6af135190fda6d0a1936cd7a0b04b3aa04ea7d99167a9e05
|
||||
size 3333014
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ca1322cc59d0a50d008d3a8406cad2bd932c4e18656cb7af061da22847dd8d1
|
||||
size 5835390
|
||||
oid sha256:66f4547ed2e83b302d795875588d9a085af76071a480f1096f2bb64344b80c42
|
||||
size 5428670
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dfbb4dca6907497e89f81687adb3fc993727b569c035a33829e069473874e40d
|
||||
size 6532718
|
||||
oid sha256:75c8cdeefbbdcab2a240821d3410539f2a2cbe0a015897f4135404c80c3ac32c
|
||||
size 6578366
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75e10d1939442b4d2bd78766bb99e7a8a7aa9b15b7c3094ee974c387d35afb81
|
||||
size 6504399
|
||||
oid sha256:92ed41854fe852ae54aa5f05de8ceaf35c3ad8ef82b3034e67edf758d1acdf50
|
||||
size 13593713
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46ebcbaf3db613e0e77d4ed0c170c58c5c5c584eb4d8a078b514e6c2c933a829
|
||||
size 6933630
|
||||
oid sha256:8507c61df25981dbe7e5bd2f9ed16c9a0befbca218947cad29f6679c77a695a7
|
||||
size 12451891
|
||||
|
||||
@@ -21,6 +21,7 @@ import uvicorn
|
||||
|
||||
from compose_farm.config import Config as CFConfig
|
||||
from compose_farm.config import load_config
|
||||
from compose_farm.state import load_state as _original_load_state
|
||||
from compose_farm.web.app import create_app
|
||||
from compose_farm.web.cdn import CDN_ASSETS, ensure_vendor_cache
|
||||
|
||||
@@ -36,11 +37,9 @@ DEMO_EXCLUDE_STACKS = {"arr"}
|
||||
def _get_filtered_config() -> CFConfig:
|
||||
"""Load config but filter out excluded stacks."""
|
||||
config = load_config()
|
||||
# Filter out excluded stacks
|
||||
filtered_stacks = {
|
||||
name: host for name, host in config.stacks.items() if name not in DEMO_EXCLUDE_STACKS
|
||||
}
|
||||
# Create a new config with filtered stacks
|
||||
return CFConfig(
|
||||
compose_dir=config.compose_dir,
|
||||
hosts=config.hosts,
|
||||
@@ -51,6 +50,12 @@ def _get_filtered_config() -> CFConfig:
|
||||
)
|
||||
|
||||
|
||||
def _get_filtered_state(config: CFConfig) -> dict[str, str | list[str]]:
|
||||
"""Load state but filter out excluded stacks."""
|
||||
state = _original_load_state(config)
|
||||
return {name: host for name, host in state.items() if name not in DEMO_EXCLUDE_STACKS}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def vendor_cache(request: pytest.FixtureRequest) -> Path:
|
||||
"""Download CDN assets once and cache to disk for faster recordings."""
|
||||
@@ -77,9 +82,11 @@ def server_url() -> Generator[str, None, None]:
|
||||
"""Start demo server using real config (with filtered stacks) and return URL."""
|
||||
os.environ["CF_CONFIG"] = str(REAL_CONFIG_PATH)
|
||||
|
||||
# Patch get_config in all web modules to filter out excluded stacks
|
||||
# Must patch where it's imported, not where it's defined
|
||||
# Patch at source module level so all callers get filtered versions
|
||||
patches = [
|
||||
# Patch load_state at source - all functions calling it get filtered state
|
||||
patch("compose_farm.state.load_state", _get_filtered_state),
|
||||
# Patch get_config where imported
|
||||
patch("compose_farm.web.routes.pages.get_config", _get_filtered_config),
|
||||
patch("compose_farm.web.routes.api.get_config", _get_filtered_config),
|
||||
patch("compose_farm.web.routes.actions.get_config", _get_filtered_config),
|
||||
@@ -87,7 +94,6 @@ def server_url() -> Generator[str, None, None]:
|
||||
patch("compose_farm.web.ws.get_config", _get_filtered_config),
|
||||
]
|
||||
|
||||
# Start all patches
|
||||
for p in patches:
|
||||
p.start()
|
||||
|
||||
@@ -104,7 +110,6 @@ def server_url() -> Generator[str, None, None]:
|
||||
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
server_ready = False
|
||||
# Wait up to 5 seconds for server to start
|
||||
for _ in range(50):
|
||||
try:
|
||||
urllib.request.urlopen(url, timeout=0.5) # noqa: S310
|
||||
@@ -123,7 +128,6 @@ def server_url() -> Generator[str, None, None]:
|
||||
thread.join(timeout=2)
|
||||
os.environ.pop("CF_CONFIG", None)
|
||||
|
||||
# Stop all patches
|
||||
for p in patches:
|
||||
p.stop()
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""Demo: Full workflow.
|
||||
|
||||
Records a ~45 second demo combining multiple features:
|
||||
- Dashboard overview with stats
|
||||
- Sidebar filtering
|
||||
- Stack navigation
|
||||
- Terminal streaming
|
||||
- Theme switching
|
||||
Records a comprehensive demo (~60 seconds) combining all major features:
|
||||
1. Console page: terminal with fastfetch, cf pull command
|
||||
2. Editor showing Compose Farm YAML config
|
||||
3. Command palette navigation to grocy stack
|
||||
4. Stack actions: up, logs
|
||||
5. Switch to mealie stack via command palette, run update
|
||||
6. Dashboard overview
|
||||
7. Theme cycling via command palette
|
||||
|
||||
This demo is used on the homepage and Web UI page as the main showcase.
|
||||
|
||||
Run: pytest docs/demos/web/demo_workflow.py -v --no-cov
|
||||
"""
|
||||
@@ -21,68 +25,167 @@ if TYPE_CHECKING:
|
||||
from playwright.sync_api import Page
|
||||
|
||||
|
||||
def _demo_dashboard_and_filter(page: Page, server_url: str) -> None:
|
||||
"""Demo part 1: Dashboard overview and sidebar filtering."""
|
||||
def _demo_console_terminal(page: Page, server_url: str) -> None:
|
||||
"""Demo part 1: Console page with terminal and editor."""
|
||||
# Start on dashboard briefly
|
||||
page.goto(server_url)
|
||||
wait_for_sidebar(page)
|
||||
pause(page, 1500)
|
||||
pause(page, 800)
|
||||
|
||||
stats_cards = page.locator("#stats-cards .card")
|
||||
if stats_cards.count() > 0:
|
||||
stats_cards.first.hover()
|
||||
pause(page, 800)
|
||||
|
||||
filter_input = page.locator("#sidebar-filter")
|
||||
filter_input.click()
|
||||
# Navigate to Console page via command palette
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#sidebar-filter", "jelly", delay=150)
|
||||
filter_input.dispatch_event("keyup")
|
||||
slow_type(page, "#cmd-input", "cons", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
page.wait_for_url("**/console", timeout=5000)
|
||||
pause(page, 800)
|
||||
|
||||
# Wait for terminal to be ready
|
||||
page.wait_for_selector("#console-terminal .xterm", timeout=10000)
|
||||
pause(page, 1000)
|
||||
|
||||
|
||||
def _demo_stack_and_logs(page: Page) -> None:
|
||||
"""Demo part 2: Navigate to stack and view logs."""
|
||||
page.locator("#sidebar-stacks a", has_text="jellyfin").click()
|
||||
page.wait_for_url("**/stack/jellyfin", timeout=5000)
|
||||
pause(page, 1500)
|
||||
|
||||
open_command_palette(page)
|
||||
pause(page, 400)
|
||||
slow_type(page, "#cmd-input", "logs", delay=150)
|
||||
pause(page, 500)
|
||||
# Run fastfetch first
|
||||
slow_type(page, "#console-terminal .xterm-helper-textarea", "fastfetch", delay=60)
|
||||
pause(page, 200)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 2000) # Wait for output
|
||||
|
||||
# Run cf pull on a stack to show Compose Farm in action
|
||||
slow_type(page, "#console-terminal .xterm-helper-textarea", "cf pull grocy", delay=60)
|
||||
pause(page, 200)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 3000) # Wait for pull output
|
||||
|
||||
|
||||
def _demo_config_editor(page: Page) -> None:
|
||||
"""Demo part 2: Show the Compose Farm config in editor."""
|
||||
# Smoothly scroll down to show the Editor section
|
||||
# Use JavaScript for smooth scrolling animation
|
||||
page.evaluate("""
|
||||
const editor = document.getElementById('console-editor');
|
||||
if (editor) {
|
||||
editor.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
""")
|
||||
pause(page, 1200) # Wait for smooth scroll animation
|
||||
|
||||
# Wait for Monaco editor to load with config content
|
||||
page.wait_for_selector("#console-editor .monaco-editor", timeout=10000)
|
||||
pause(page, 2000) # Let viewer see the Compose Farm config file
|
||||
|
||||
|
||||
def _demo_stack_actions(page: Page) -> None:
|
||||
"""Demo part 3: Navigate to stack and run actions."""
|
||||
# Click on sidebar to take focus away from terminal, then use command palette
|
||||
page.locator("#sidebar-stacks").click()
|
||||
pause(page, 300)
|
||||
|
||||
# Navigate to grocy via command palette
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "grocy", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
page.wait_for_url("**/stack/grocy", timeout=5000)
|
||||
pause(page, 1000)
|
||||
|
||||
# Open Compose File editor to show the compose.yaml
|
||||
compose_collapse = page.locator(".collapse", has_text="Compose File").first
|
||||
compose_collapse.locator("input[type=checkbox]").click(force=True)
|
||||
pause(page, 500)
|
||||
|
||||
# Wait for Monaco editor to load and show content
|
||||
page.wait_for_selector("#compose-editor .monaco-editor", timeout=10000)
|
||||
pause(page, 2000) # Let viewer see the compose file
|
||||
|
||||
# Close the compose file section
|
||||
compose_collapse.locator("input[type=checkbox]").click(force=True)
|
||||
pause(page, 500)
|
||||
|
||||
# Run Up action via command palette
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "up", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 200)
|
||||
|
||||
# Wait for terminal output
|
||||
page.wait_for_selector("#terminal-output .xterm", timeout=5000)
|
||||
pause(page, 2500)
|
||||
|
||||
# Show logs
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "logs", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 200)
|
||||
|
||||
page.wait_for_selector("#terminal-output .xterm", timeout=5000)
|
||||
pause(page, 3000)
|
||||
pause(page, 2500)
|
||||
|
||||
|
||||
def _demo_theme_and_return(page: Page, server_url: str) -> None:
|
||||
"""Demo part 3: Switch theme and return to dashboard."""
|
||||
# Switch to mealie via command palette
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "mealie", delay=100)
|
||||
pause(page, 400)
|
||||
slow_type(page, "#cmd-input", "theme: luxury", delay=100)
|
||||
pause(page, 600)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 1500)
|
||||
|
||||
open_command_palette(page)
|
||||
pause(page, 400)
|
||||
slow_type(page, "#cmd-input", "dash", delay=150)
|
||||
pause(page, 500)
|
||||
page.keyboard.press("Enter")
|
||||
page.wait_for_url(server_url, timeout=5000)
|
||||
pause(page, 1500)
|
||||
|
||||
page.locator("#sidebar-filter").fill("")
|
||||
page.locator("#sidebar-filter").dispatch_event("keyup")
|
||||
page.wait_for_url("**/stack/mealie", timeout=5000)
|
||||
pause(page, 1000)
|
||||
|
||||
# Run update action
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "upda", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 200)
|
||||
|
||||
page.wait_for_selector("#terminal-output .xterm", timeout=5000)
|
||||
pause(page, 2500)
|
||||
|
||||
|
||||
def _demo_dashboard_and_themes(page: Page, server_url: str) -> None:
|
||||
"""Demo part 4: Dashboard and theme cycling."""
|
||||
# Navigate to dashboard via command palette
|
||||
open_command_palette(page)
|
||||
pause(page, 300)
|
||||
slow_type(page, "#cmd-input", "dash", delay=100)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
page.wait_for_url(server_url, timeout=5000)
|
||||
pause(page, 800)
|
||||
|
||||
# Scroll to top of page to ensure dashboard is fully visible
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
pause(page, 600)
|
||||
|
||||
# Open theme picker and arrow down to Luxury (shows live preview)
|
||||
# Theme order: light, dark, cupcake, bumblebee, emerald, corporate, synthwave,
|
||||
# retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel,
|
||||
# fantasy, wireframe, black, luxury (index 19)
|
||||
page.locator("#theme-btn").click()
|
||||
page.wait_for_selector("#cmd-palette[open]", timeout=2000)
|
||||
pause(page, 400)
|
||||
|
||||
# Arrow down through themes with live preview until we reach Luxury
|
||||
for _ in range(19):
|
||||
page.keyboard.press("ArrowDown")
|
||||
pause(page, 180)
|
||||
|
||||
# Select Luxury theme
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 1000)
|
||||
|
||||
# Return to dark theme
|
||||
page.locator("#theme-btn").click()
|
||||
page.wait_for_selector("#cmd-palette[open]", timeout=2000)
|
||||
pause(page, 300)
|
||||
page.locator("#cmd-input").fill("theme: dark")
|
||||
pause(page, 500)
|
||||
slow_type(page, "#cmd-input", " dark", delay=80)
|
||||
pause(page, 400)
|
||||
page.keyboard.press("Enter")
|
||||
pause(page, 1000)
|
||||
|
||||
@@ -92,6 +195,7 @@ def test_demo_workflow(recording_page: Page, server_url: str) -> None:
|
||||
"""Record full workflow demo."""
|
||||
page = recording_page
|
||||
|
||||
_demo_dashboard_and_filter(page, server_url)
|
||||
_demo_stack_and_logs(page)
|
||||
_demo_theme_and_return(page, server_url)
|
||||
_demo_console_terminal(page, server_url)
|
||||
_demo_config_editor(page)
|
||||
_demo_stack_actions(page)
|
||||
_demo_dashboard_and_themes(page, server_url)
|
||||
|
||||
@@ -22,12 +22,9 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Colors for output
|
||||
GREEN = "\033[0;32m"
|
||||
BLUE = "\033[0;34m"
|
||||
YELLOW = "\033[0;33m"
|
||||
RED = "\033[0;31m"
|
||||
NC = "\033[0m" # No Color
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
REPO_DIR = SCRIPT_DIR.parent.parent.parent
|
||||
@@ -88,16 +85,16 @@ def patch_playwright_video_quality() -> None:
|
||||
# Replace with high-quality settings
|
||||
new_content = re.sub(pattern, VIDEO_QUALITY_ARGS, content)
|
||||
video_recorder.write_text(new_content)
|
||||
print(f"{GREEN}Patched Playwright for high-quality video recording{NC}")
|
||||
console.print("[green]Patched Playwright for high-quality video recording[/green]")
|
||||
|
||||
|
||||
def record_demo(name: str) -> Path | None:
|
||||
"""Run a single demo and return the video path."""
|
||||
print(f"{GREEN}Recording:{NC} web-{name}")
|
||||
console.print(f"[green]Recording:[/green] web-{name}")
|
||||
|
||||
demo_file = SCRIPT_DIR / f"demo_{name}.py"
|
||||
if not demo_file.exists():
|
||||
print(f"{RED} Demo file not found: {demo_file}{NC}")
|
||||
console.print(f"[red] Demo file not found: {demo_file}[/red]")
|
||||
return None
|
||||
|
||||
# Create temp output dir for this recording
|
||||
@@ -126,20 +123,20 @@ def record_demo(name: str) -> Path | None:
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"{RED} Failed to record {name}{NC}")
|
||||
print(result.stdout)
|
||||
print(result.stderr)
|
||||
console.print(f"[red] Failed to record {name}[/red]")
|
||||
console.print(result.stdout)
|
||||
console.print(result.stderr)
|
||||
return None
|
||||
|
||||
# Find the recorded video
|
||||
videos = list(temp_dir.rglob("*.webm"))
|
||||
if not videos:
|
||||
print(f"{RED} No video found for {name}{NC}")
|
||||
console.print(f"[red] No video found for {name}[/red]")
|
||||
return None
|
||||
|
||||
# Use the most recent video
|
||||
video = max(videos, key=lambda p: p.stat().st_mtime)
|
||||
print(f"{GREEN} Recorded: {video.name}{NC}")
|
||||
console.print(f"[green] Recorded: {video.name}[/green]")
|
||||
return video
|
||||
|
||||
|
||||
@@ -193,10 +190,10 @@ def move_recording(video_path: Path, name: str) -> tuple[Path, Path]:
|
||||
webm_dest = OUTPUT_DIR / f"{output_name}.webm"
|
||||
|
||||
shutil.copy2(video_path, webm_dest)
|
||||
print(f"{BLUE} WebM: {webm_dest.relative_to(REPO_DIR)}{NC}")
|
||||
console.print(f"[blue] WebM: {webm_dest.relative_to(REPO_DIR)}[/blue]")
|
||||
|
||||
gif_path = convert_to_gif(video_path, output_name)
|
||||
print(f"{BLUE} GIF: {gif_path.relative_to(REPO_DIR)}{NC}")
|
||||
console.print(f"[blue] GIF: {gif_path.relative_to(REPO_DIR)}[/blue]")
|
||||
|
||||
return webm_dest, gif_path
|
||||
|
||||
@@ -210,9 +207,9 @@ def cleanup() -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""Record all web UI demos."""
|
||||
print(f"{BLUE}Recording web UI demos...{NC}")
|
||||
print(f"Output directory: {OUTPUT_DIR}")
|
||||
print()
|
||||
console.print("[blue]Recording web UI demos...[/blue]")
|
||||
console.print(f"Output directory: {OUTPUT_DIR}")
|
||||
console.print()
|
||||
|
||||
# Patch Playwright for high-quality video recording
|
||||
patch_playwright_video_quality()
|
||||
@@ -221,7 +218,7 @@ def main() -> int:
|
||||
if len(sys.argv) > 1:
|
||||
demos_to_record = [d for d in sys.argv[1:] if d in DEMOS]
|
||||
if not demos_to_record:
|
||||
print(f"{RED}Unknown demo(s). Available: {', '.join(DEMOS)}{NC}")
|
||||
console.print(f"[red]Unknown demo(s). Available: {', '.join(DEMOS)}[/red]")
|
||||
return 1
|
||||
else:
|
||||
demos_to_record = DEMOS
|
||||
@@ -230,7 +227,7 @@ def main() -> int:
|
||||
|
||||
try:
|
||||
for i, demo in enumerate(demos_to_record, 1):
|
||||
print(f"{YELLOW}=== Demo {i}/{len(demos_to_record)}: {demo} ==={NC}")
|
||||
console.print(f"[yellow]=== Demo {i}/{len(demos_to_record)}: {demo} ===[/yellow]")
|
||||
|
||||
video_path = record_demo(demo)
|
||||
if video_path:
|
||||
@@ -238,23 +235,23 @@ def main() -> int:
|
||||
results[demo] = (webm, gif)
|
||||
else:
|
||||
results[demo] = (None, None)
|
||||
print()
|
||||
console.print()
|
||||
finally:
|
||||
cleanup()
|
||||
|
||||
# Summary
|
||||
print(f"{BLUE}=== Summary ==={NC}")
|
||||
console.print("[blue]=== Summary ===[/blue]")
|
||||
success_count = sum(1 for w, _ in results.values() if w is not None)
|
||||
print(f"Recorded: {success_count}/{len(demos_to_record)} demos")
|
||||
print()
|
||||
console.print(f"Recorded: {success_count}/{len(demos_to_record)} demos")
|
||||
console.print()
|
||||
|
||||
for demo, (webm, gif) in results.items(): # type: ignore[assignment]
|
||||
status = f"{GREEN}OK{NC}" if webm else f"{RED}FAILED{NC}"
|
||||
print(f" {demo}: {status}")
|
||||
status = "[green]OK[/green]" if webm else "[red]FAILED[/red]"
|
||||
console.print(f" {demo}: {status}")
|
||||
if webm:
|
||||
print(f" {webm.relative_to(REPO_DIR)}")
|
||||
console.print(f" {webm.relative_to(REPO_DIR)}")
|
||||
if gif:
|
||||
print(f" {gif.relative_to(REPO_DIR)}")
|
||||
console.print(f" {gif.relative_to(REPO_DIR)}")
|
||||
|
||||
return 0 if success_count == len(demos_to_record) else 1
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ Then open [http://localhost:8000](http://localhost:8000).
|
||||
|
||||
## Features
|
||||
|
||||
### Command Palette
|
||||
### Full Workflow
|
||||
|
||||
Press `Ctrl+K` (or `Cmd+K` on macOS) to open the command palette. Use fuzzy search to quickly navigate, trigger actions, or change themes.
|
||||
Console terminal, config editor, stack navigation, actions (up, logs, update), dashboard overview, and theme switching - all in one flow.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-navigation.webm" type="video/webm">
|
||||
<source src="/assets/web-workflow.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Stack Actions
|
||||
@@ -38,12 +38,12 @@ Navigate to any stack and use the command palette to trigger actions like restar
|
||||
<source src="/assets/web-themes.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
### Full Workflow
|
||||
### Command Palette
|
||||
|
||||
Dashboard overview, sidebar filtering, stack navigation, terminal streaming, and theme switching - all in one flow.
|
||||
Press `Ctrl+K` (or `Cmd+K` on macOS) to open the command palette. Use fuzzy search to quickly navigate, trigger actions, or change themes.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/web-workflow.webm" type="video/webm">
|
||||
<source src="/assets/web-navigation.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
## Pages
|
||||
|
||||
Reference in New Issue
Block a user