diff --git a/fleet-canvas b/fleet-canvas new file mode 100755 index 0000000..9778241 --- /dev/null +++ b/fleet-canvas @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +""" +FLEET CANVAS STITCHER & PHYSICAL MONITOR MATRIX +Captures screens across the fleet (t520, bazina, boxC, penguin, android), +stitches them according to the physical table/monitor canvas, +aggregates context from ~/src/dnes, and saves into ~/.agents across fleet. +""" + +import os +import sys +import json +import glob +import re +import subprocess +from datetime import datetime +from PIL import Image, ImageDraw, ImageFont, ImageFilter + +HOME = os.path.expanduser("~") +AGENTS_DIR = os.path.join(HOME, ".agents") +SRC_AGENTS_DIR = os.path.join(HOME, "src", ".agents") +os.makedirs(AGENTS_DIR, exist_ok=True) +os.makedirs(SRC_AGENTS_DIR, exist_ok=True) + +# 1. Capture Screenshots +def capture_fleet_screenshots(): + print("[*] Capturing screenshots across fleet nodes...") + + # t520 local + t520_path = "/tmp/scrot_t520.png" + try: + subprocess.run(["scrot", "-z", "-o", t520_path], check=True, env=dict(os.environ, DISPLAY=":0")) + print(f" [βœ“] t520: captured ({t520_path})") + except Exception as e: + print(f" [-] t520 scrot error: {e}") + + # bazina remote + bazina_path = "/tmp/scrot_bazina.png" + try: + cmd = 'DISPLAY=:0 scrot -z -o /tmp/scrot_bazina.png 2>/dev/null || DISPLAY=:0 XAUTHORITY=/home/kof/.Xauthority scrot -z -o /tmp/scrot_bazina.png' + subprocess.run(["ssh", "-o", "ConnectTimeout=3", "bazina", cmd], check=True, timeout=5) + subprocess.run(["scp", "-o", "ConnectTimeout=3", "bazina:/tmp/scrot_bazina.png", bazina_path], check=True, timeout=5) + print(f" [βœ“] bazina: captured and transferred ({bazina_path})") + except Exception as e: + print(f" [-] bazina scrot error: {e}") + + # boxC remote + boxc_path = "/tmp/scrot_boxc.png" + try: + cmd = 'DISPLAY=:0 scrot -z -o /tmp/scrot_boxc.png 2>/dev/null || sudo -u root DISPLAY=:0 scrot -z -o /tmp/scrot_boxc.png' + subprocess.run(["ssh", "-o", "ConnectTimeout=3", "boxC", cmd], check=True, timeout=5) + subprocess.run(["scp", "-o", "ConnectTimeout=3", "boxC:/tmp/scrot_boxc.png", boxc_path], check=True, timeout=5) + print(f" [βœ“] boxC: captured and transferred ({boxc_path})") + except Exception as e: + print(f" [-] boxC scrot error: {e}") + + return { + "t520": t520_path if os.path.exists(t520_path) else None, + "bazina": bazina_path if os.path.exists(bazina_path) else None, + "boxC": boxc_path if os.path.exists(boxc_path) else None + } + +# 2. Harvest Context from ~/src/dnes +def harvest_dnes_context(): + journal_path = os.path.join(HOME, "src", "dnes", "journal.md") + milestones = [] + if os.path.exists(journal_path): + with open(journal_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line.startswith("- ["): + milestones.append(line) + + # Moonphase + moon_summary = "🌘 UbΓ½vajΓ­cΓ­ srpek (15.4% osvΔ›tlenΓ­)" + try: + res = subprocess.run(["python3", "-c", "import sys; sys.path.insert(0, os.path.expanduser('~/bin')); import moonphase; print(moonphase.get_moon_metrics().get('summary'))"], capture_output=True, text=True) + if res.stdout.strip(): + moon_summary = res.stdout.strip() + except Exception: + pass + + return { + "recent_milestones": milestones[-8:] if milestones else [], + "moon_summary": moon_summary, + "time_str": datetime.now().strftime("%Y-%m-%d %H:%M:%S CET") + } + +# 3. Create Placeholder Screen Card +def create_placeholder_card(title, width, height, ip, details): + img = Image.new("RGB", (width, height), (18, 22, 28)) + draw = ImageDraw.Draw(img) + + # Grid lines + for x in range(0, width, 40): + draw.line([(x, 0), (x, height)], fill=(28, 34, 44), width=1) + for y in range(0, height, 40): + draw.line([(0, y), (width, y)], fill=(28, 34, 44), width=1) + + # Frame border + draw.rectangle([(2, 2), (width - 3, height - 3)], outline=(60, 80, 110), width=2) + + # Title bar + draw.rectangle([(2, 2), (width - 3, 34)], fill=(32, 45, 62)) + draw.text((12, 8), title, fill=(220, 235, 255)) + draw.text((width - 140, 8), ip, fill=(120, 190, 255)) + + # Center terminal box + tw, th = int(width * 0.85), int(height * 0.65) + tx, ty = (width - tw) // 2, (height - th) // 2 + 10 + draw.rectangle([(tx, ty), (tx + tw, ty + th)], fill=(10, 12, 16), outline=(45, 60, 80), width=2) + + # Text inside terminal + y_text = ty + 18 + draw.text((tx + 20, y_text), f"$ fleet-node-telemetry --node={title.split()[0].lower()}", fill=(120, 255, 120)) + y_text += 26 + for line in details: + draw.text((tx + 20, y_text), line, fill=(180, 200, 220)) + y_text += 22 + + # Status indicator + draw.ellipse([(tx + tw - 24, ty + 12), (tx + tw - 12, ty + 24)], fill=(0, 230, 120)) + return img + +# 4. Render Physical Canvas +def render_canvas(screens, ctx): + print("[*] Composing physical display canvas matrix...") + + # Canvas dimensions + CANVAS_W = 4920 + CANVAS_H = 2280 + BG_COLOR = (20, 24, 30) + GRID_COLOR = (30, 36, 46) + + canvas = Image.new("RGB", (CANVAS_W, CANVAS_H), BG_COLOR) + draw = ImageDraw.Draw(canvas) + + # Draw desk blueprint grid + for x in range(0, CANVAS_W, 60): + draw.line([(x, 0), (x, CANVAS_H)], fill=GRID_COLOR, width=1) + for y in range(0, CANVAS_H, 60): + draw.line([(0, y), (CANVAS_W, y)], fill=GRID_COLOR, width=1) + + # Header Bar + draw.rectangle([(0, 0), (CANVAS_W, 110)], fill=(12, 15, 20), outline=(40, 50, 65), width=2) + draw.text((36, 20), "THE OBSERVATOŘ FLEET // PHYSICAL DISPLAY CANVAS MATRIX", fill=(255, 255, 255)) + draw.text((36, 54), f"SYNERGY KVM 5-NODE MULTI-HEAD MESH ⨂ {ctx['time_str']} ⨂ {ctx['moon_summary']}", fill=(121, 172, 228)) # FCZ blue #79ACE4 + draw.text((36, 80), "PTP GRANDMASTER (Intel 82579LM Β±6ns) ⨂ 50.000 Hz EU GRID TELEMETRY ⨂ ARCHER VR400 192.168.1.0/24", fill=(160, 175, 195)) + + # Top right badges + draw.rectangle([(CANVAS_W - 540, 20), (CANVAS_W - 36, 90)], fill=(18, 24, 34), outline=(70, 90, 120), width=1) + draw.text((CANVAS_W - 520, 32), "FLEET NETWORK HIERARCHY:", fill=(255, 220, 100)) + draw.text((CANVAS_W - 520, 52), "L1: LAN 192.168.1.0/24 (Gigabit + WiFi)", fill=(200, 220, 240)) + draw.text((CANVAS_W - 520, 70), "L2: WireGuard 10.39.9.0/24 Β· L3: OpenVPN", fill=(140, 180, 220)) + + # Monitor Bezel & Stand helper + def paste_monitor(img, x, y, title, badge, is_server=False): + mw, mh = img.size + bw = 14 # Bezel thickness + # Bezel frame + bezel_x1, bezel_y1 = x - bw, y - bw - 28 + bezel_x2, bezel_y2 = x + mw + bw, y + mh + bw + 14 + + # Outer shadow + draw.rectangle([(bezel_x1 + 6, bezel_y1 + 6), (bezel_x2 + 6, bezel_y2 + 6)], fill=(10, 12, 16)) + # Bezel body + bezel_color = (35, 42, 52) if not is_server else (45, 56, 70) + draw.rectangle([(bezel_x1, bezel_y1), (bezel_x2, bezel_y2)], fill=bezel_color, outline=(70, 85, 105), width=2) + + # Title bar on bezel + draw.text((bezel_x1 + 14, bezel_y1 + 6), title, fill=(255, 255, 255)) + draw.text((bezel_x2 - len(badge)*8 - 14, bezel_y1 + 6), badge, fill=(120, 220, 160) if not is_server else (255, 200, 80)) + + # Power LED + draw.ellipse([(bezel_x2 - 18, bezel_y2 - 12), (bezel_x2 - 8, bezel_y2 - 2)], fill=(0, 255, 130)) + + # Paste actual screen + canvas.paste(img, (x, y)) + + # Desk stand + sw, sh = 120, 30 + sx = x + (mw - sw) // 2 + draw.rectangle([(sx, bezel_y2), (sx + sw, bezel_y2 + 10)], fill=(50, 60, 75)) + draw.rectangle([(sx - 30, bezel_y2 + 10), (sx + sw + 30, bezel_y2 + 18)], fill=(30, 36, 45), outline=(60, 75, 95), width=1) + + # 1. BAZINA (Left-Top) + bazina_img = None + if screens.get("bazina") and os.path.exists(screens["bazina"]): + bazina_img = Image.open(screens["bazina"]).convert("RGB") + else: + bazina_img = create_placeholder_card("BAZINA [LEFT-TOP]", 1920, 1080, "192.168.1.104", [ + "OS: Debian GNU/Linux 13 (Trixie)", + "CPU: AMD Ryzen 5 3600 (6c/12t) @ 4.2 GHz", + "GPU: AMD Radeon RX 5600/5700 XT (Navi 10)", + "Audio: ASUS VX248 HDMI (WirePlumber p1500)", + "NBD Swap Server: 8GB tmpfs ramdisk -> t520", + "Print/Scan: Brother DCP-J4120DW (CUPS 600 DPI)", + "Storage: /mnt/ssd (1TB NVMe), /mnt/seagate (1TB)" + ]) + paste_monitor(bazina_img, 60, 160, "BAZINA (LEFT-TOP) // AMD RYZEN + RX 5700 XT", "192.168.1.104 [1920x1080]") + + # 2. PENGUIN (Left-Bottom) + penguin_img = create_placeholder_card("PENGUIN [LEFT-BOTTOM]", 1366, 768, "192.168.1.105", [ + "Hardware: Chromebook Field Node", + "Identity: crostini [x86_64 Debian container]", + "Role: Mobile field terminal & documentation", + "Synergy KVM: Client connected (bottom-left)", + "NTP: Synchronized to t520 Stratum 1 Grandmaster", + "Network: 192.168.1.105 (WiFi) / 10.39.9.105 (WG)", + "Physical placement: Left of t520 under bazina" + ]) + paste_monitor(penguin_img, 60, 1340, "PENGUIN (LEFT-BOTTOM) // CHROMEBOOK FIELD NODE", "192.168.1.105 [1366x768]") + + # 3. ANDROID (Under table left) + android_img = create_placeholder_card("ANDROID [PHONE]", 470, 768, "192.168.1.103", [ + "Device: Xiaomi 22101316G", + "OS: Android 14 / Termux aarch64", + "Port: 8022 (u0_a322)", + "Sensors: OSC 9000/8000 -> bazina", + "Sync: Syncthing phone-DCIM", + "Status: Pod stolem vlevo" + ]) + paste_monitor(android_img, 60 + 1366 + 84, 1340, "ANDROID // TERMUX aarch64", "192.168.1.103:8022") + + # 4. T520 (Center KVM Server) + t520_img = None + if screens.get("t520") and os.path.exists(screens["t520"]): + t520_img = Image.open(screens["t520"]).convert("RGB") + else: + t520_img = create_placeholder_card("T520 [CENTER / SERVER]", 1600, 900, "192.168.1.100", [ + "Hardware: ThinkPad t520 (Intel i5-2520M)", + "OS: Arch Linux x86_64", + "Role: Synergy KVM Server (Port 24800)", + "IEEE 1588 PTP Grandmaster: Intel 82579LM (/dev/ptp0)", + "SDR Heat Scanner: RTL-SDR 868.95 MHz Wireless M-Bus", + "Memory: 8GB zram0 (zstd) + 8GB NBD RAM swap bazina", + "Power/ACPI: S3 freeze disabled, lid mute hook active" + ]) + paste_monitor(t520_img, 2060, 480, "T520 (CENTER SERVER / GRANDMASTER) // THINKPAD T520", "192.168.1.100 [1600x900]", is_server=True) + + # 5. BOXC (Right Node) + boxc_img = None + if screens.get("boxC") and os.path.exists(screens["boxC"]): + boxc_img = Image.open(screens["boxC"]).convert("RGB") + else: + boxc_img = create_placeholder_card("BOXC [RIGHT]", 1024, 768, "192.168.1.101", [ + "Hardware: Raspberry Pi 4 Model B (4GB RAM)", + "OS: Debian GNU/Linux 12 (Bookworm aarch64)", + "Display: PiJockey HDMI CRTC scanout (1024x768)", + "VPN Host: WireGuard 10.39.9.1 / OpenVPN 10.39.10.1", + "Storage: /mnt/kof17 (916GB USB disk NFS/SMB)", + "Web: peΕ‘ek.com / Konzdat publishing origin" + ]) + paste_monitor(boxc_img, 3760, 546, "BOXC (RIGHT) // RASPBERRY PI 4 VPN & STORAGE", "192.168.1.101 [1024x768]") + + # Traversal Vectors (Synergy KVM Traversal Lines) + arrow_color = (255, 190, 40) # Bright Amber + + # t520 <---> boxC + draw.line([(2060 + 1600 + 14, 480 + 450), (3760 - 14, 546 + 384)], fill=arrow_color, width=3) + draw.text((3700 - 80, 500), "<=== Synergy KVM ===>", fill=arrow_color) + + # t520 <---> penguin + draw.line([(2060 - 14, 480 + 450), (60 + 1366 + 14, 1340 + 200)], fill=arrow_color, width=3) + draw.text((1520, 880), "<=== Synergy KVM ===>", fill=arrow_color) + + # t520 <---> bazina + draw.line([(2060 + 400, 480 - 14), (60 + 1600, 160 + 1080 + 14)], fill=arrow_color, width=3) + draw.text((1700, 300), "<=== Synergy KVM ===>", fill=arrow_color) + + # bazina <---> penguin + draw.line([(60 + 600, 160 + 1080 + 14), (60 + 600, 1340 - 14)], fill=arrow_color, width=3) + draw.text((60 + 620, 1260), "KVM <^>", fill=arrow_color) + + # bazina <---> boxC + draw.line([(60 + 1920 + 14, 160 + 300), (3760 - 14, 546 + 200)], fill=(100, 180, 255), width=2) + draw.text((2900, 220), "<-- NFSv4 Gigabit Link (/mnt/ssd, /mnt/kof17) -->", fill=(100, 180, 255)) + + # Bottom Context Footer Bar (Aggregated from ~/src/dnes) + draw.rectangle([(0, CANVAS_H - 130), (CANVAS_W, CANVAS_H)], fill=(12, 15, 20), outline=(40, 50, 65), width=2) + draw.text((36, CANVAS_H - 118), "POSLEDNÍ ZÁPISY A MILNÍKY Z REPOZITÁŘE DNES (JOURNAL.MD):", fill=(255, 220, 80)) + + col1_x = 36 + col2_x = CANVAS_W // 2 + for i, m in enumerate(ctx["recent_milestones"][:6]): + target_x = col1_x if i < 3 else col2_x + target_y = CANVAS_H - 92 + (i % 3) * 26 + # clean milestone text + clean_m = m if len(m) < 140 else m[:137] + "..." + draw.text((target_x, target_y), clean_m, fill=(200, 215, 235)) + + # Save files + out_png = os.path.join(AGENTS_DIR, "fleet_canvas.png") + out_jpg = os.path.join(AGENTS_DIR, "fleet_canvas.jpg") + out_md = os.path.join(AGENTS_DIR, "fleet_canvas.md") + + print(f"[*] Saving master canvas to {out_png}...") + canvas.save(out_png, "PNG", optimize=True) + + # Also save 0.5x preview JPEG for small-web viewing + print(f"[*] Saving preview JPEG to {out_jpg}...") + preview = canvas.resize((CANVAS_W // 2, CANVAS_H // 2), Image.Resampling.LANCZOS) + preview.save(out_jpg, "JPEG", quality=88) + + # Write Markdown documentation + with open(out_md, "w", encoding="utf-8") as f: + f.write(f"""# The ObservatoΕ™ Fleet // Physical Display Canvas Matrix + +VygenerovΓ‘no: {ctx['time_str']} +LunΓ‘rnΓ­ fΓ‘ze: {ctx['moon_summary']} + +![Fleet Physical Canvas]({out_png}) + +## Topologie fyzickΓ©ho plΓ‘tna obrazovek a KVM (Synergy) +``` +╔═══════════════════════════════════════════════════════════════════════════════════════════════════════╗ +β•‘ [LEVÝ HORNÍ] BAZINA β”‚ [STŘED / KVM SERVER] T520 β”‚ [PRAVÝ] BOXC β•‘ +β•‘ 192.168.1.104 Β· 1920x1080 β”‚ 192.168.1.100 Β· 1600x900 β”‚ 192.168.1.101 Β· 1024x768β•‘ +β•‘ Debian 13 Β· Ryzen Β· RX 5700 XT β”‚ Arch Linux Β· ThinkPad Β· Intel HD 3000 β”‚ Pi 4 Β· PiJockey HDMI β•‘ +║────────────────────────────────────┴─────────────────────────────────────────┴────────────────────────║ +β•‘ [LEVÝ DOLNÍ] PENGUIN β”‚ [POD STOLEM VLEVO] ANDROID β•‘ +β•‘ 192.168.1.105 Β· 1366x768 β”‚ 192.168.1.103:8022 Β· Termux aarch64 β•‘ +β•‘ Crostini Chromebook x86_64 β”‚ SenzorickΓ‘ telemetrie OSC β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +``` + +## SouhrnnΓ© parametry zachycenΓ­ +- **t520 (Server):** 1600x900, Arch Linux, PTP Grandmaster (Β±6 ns), SDR 868.95 MHz radiΓ‘tory +- **bazina:** 1920x1080, Debian 13 Trixie, NBD 8GB swap server, Brother DCP-J4120DW CUPS +- **boxC:** 1024x768, Raspberry Pi 4, WireGuard VPN (10.39.9.1), OpenVPN (10.39.10.1), NFS /mnt/kof17 +- **penguin:** 1366x768, Chromebook crostini mobilnΓ­ terΓ©nnΓ­ stanice +- **android:** 192.168.1.103:8022, Xiaomi Termux aarch64 + +## PoslednΓ­ zaznamenanΓ© milnΓ­ky z repozitΓ‘Ε™e dnes: +""") + for m in ctx["recent_milestones"]: + f.write(f"{m}\n") + + # Synchronize across fleet (.agents directories) + print("[*] Synchronizing canvas across fleet nodes...") + # Copy to src/.agents + subprocess.run(["cp", out_png, out_jpg, out_md, SRC_AGENTS_DIR], check=True) + + # Copy to bazina:~/.agents/ + try: + subprocess.run(["ssh", "bazina", "mkdir -p ~/.agents"], timeout=3) + subprocess.run(["scp", "-q", out_png, out_jpg, out_md, "bazina:~/.agents/"], timeout=8) + print(" [βœ“] bazina: ~/.agents synchronized") + except Exception as e: + print(f" [-] bazina sync warning: {e}") + + # Copy to boxC:~/.agents/ and /var/www/html/konzdat/assets/ + try: + subprocess.run(["ssh", "boxC", "mkdir -p ~/.agents /var/www/html/konzdat/assets"], timeout=3) + subprocess.run(["scp", "-q", out_png, out_jpg, out_md, "boxC:~/.agents/"], timeout=8) + subprocess.run(["scp", "-q", out_png, out_jpg, "boxC:/var/www/html/konzdat/assets/"], timeout=8) + subprocess.run(["ssh", "boxC", "ln -sfn /var/www/html/konzdat/assets/fleet_canvas.png /var/www/html/canvas.png"], timeout=3) + print(" [βœ“] boxC: ~/.agents and web http://peΕ‘ek.com/canvas.png synchronized") + except Exception as e: + print(f" [-] boxC sync warning: {e}") + + print(f"[βœ“] Fleet canvas complete! Master saved to {out_png}") + return out_png + +def main(): + screens = capture_fleet_screenshots() + ctx = harvest_dnes_context() + render_canvas(screens, ctx) + +if __name__ == "__main__": + main()