diff --git a/.github/assets/README.md b/.github/assets/README.md
index 37def83..1e2633f 100644
--- a/.github/assets/README.md
+++ b/.github/assets/README.md
@@ -10,17 +10,22 @@ This directory contains the visual branding, architecture infographics, UI previ
| :--- | :--- | :---: | :---: | :--- |
| **Hero Banner** | [`passli-repo-banner.png`](passli-repo-banner.png) | Static Image (PNG) | 1376 × 768 (16:9) | Primary README header banner, social card, and project cover |
| **Ephemeral Flow Demo** | [`passli-ephemeral-workflow.gif`](passli-ephemeral-workflow.gif) | Animated GIF | 1100 × 580 (58 frames) | Interactive demonstration of pass creation, 8-char key hashing, QR protocol, live countdown, and 1-click revocation |
-| **Vault Dashboard Preview** | [`passli-dashboard-preview.png`](passli-dashboard-preview.png) | Static Image (PNG) | 1376 × 768 (16:9) | High-fidelity UI mockup of the authenticated personal vault dashboard, telemetry, and document cards |
+| **Vault Dashboard Preview** | [`passli-dashboard-preview.png`](passli-dashboard-preview.png) | Static Image (PNG) | 1376 × 768 (16:9) | Framed UI preview of the live personal vault dashboard, active pass telemetry, and storage quota |
+| **Encrypted Vault Documents** | [`passli-vault-documents.png`](passli-vault-documents.png) | Static Image (PNG) | 1376 × 768 (16:9) | Framed view of the encrypted document vault with category filter pills and SHA-256 integrity inspection |
| **Cryptographic Pipeline** | [`passli-cryptographic-pipeline.png`](passli-cryptographic-pipeline.png) | Static Image (PNG) | 1376 × 768 (16:9) | Architecture diagram depicting AES-256-GCM AEAD ingestion and in-memory streaming decryption |
---
## 🛠️ Regeneration & Build Tools
-The animated workflow demonstration (`passli-ephemeral-workflow.gif`) can be deterministically generated and optimized using Pillow:
+The animated workflow demonstration (`passli-ephemeral-workflow.gif`) and dashboard UI preview (`passli-dashboard-preview.png`) are deterministically generated via Python Pillow tooling:
```bash
+# Regenerate 58-frame animated ephemeral workflow demonstration GIF
python scripts/generate_ephemeral_flow_gif.py
+
+# Regenerate pixel-perfect vault dashboard UI preview
+python scripts/generate_dashboard_preview.py
```
### Design Standards & Palette
diff --git a/.github/assets/passli-audit-trail.png b/.github/assets/passli-audit-trail.png
new file mode 100644
index 0000000..77409f4
Binary files /dev/null and b/.github/assets/passli-audit-trail.png differ
diff --git a/.github/assets/passli-dashboard-preview.png b/.github/assets/passli-dashboard-preview.png
index b6cdff1..31a40ff 100644
Binary files a/.github/assets/passli-dashboard-preview.png and b/.github/assets/passli-dashboard-preview.png differ
diff --git a/.github/assets/passli-ephemeral-workflow.gif b/.github/assets/passli-ephemeral-workflow.gif
index e5c1fe1..66d3d0a 100644
Binary files a/.github/assets/passli-ephemeral-workflow.gif and b/.github/assets/passli-ephemeral-workflow.gif differ
diff --git a/.github/assets/passli-landing-hero.png b/.github/assets/passli-landing-hero.png
new file mode 100644
index 0000000..033bd7b
Binary files /dev/null and b/.github/assets/passli-landing-hero.png differ
diff --git a/.github/assets/passli-repo-banner.png b/.github/assets/passli-repo-banner.png
index bb91245..859f51b 100644
Binary files a/.github/assets/passli-repo-banner.png and b/.github/assets/passli-repo-banner.png differ
diff --git a/.github/assets/passli-settings-page.png b/.github/assets/passli-settings-page.png
new file mode 100644
index 0000000..872604b
Binary files /dev/null and b/.github/assets/passli-settings-page.png differ
diff --git a/.github/assets/passli-share-create.png b/.github/assets/passli-share-create.png
new file mode 100644
index 0000000..953a843
Binary files /dev/null and b/.github/assets/passli-share-create.png differ
diff --git a/.github/assets/passli-share-simulator.png b/.github/assets/passli-share-simulator.png
new file mode 100644
index 0000000..354f5e6
Binary files /dev/null and b/.github/assets/passli-share-simulator.png differ
diff --git a/.github/assets/passli-vault-documents.png b/.github/assets/passli-vault-documents.png
new file mode 100644
index 0000000..d53b34b
Binary files /dev/null and b/.github/assets/passli-vault-documents.png differ
diff --git a/README.md b/README.md
index 079b8f2..f166e88 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,12 @@ When sharing sensitive records with doctors, employers, financial institutions,
- **Domain Taxonomy**: Organize records into structured categories (`Medical`, `Education`, `Vehicle`, `Personal & Identity`, `Professional`, `Other`).
+- **Encrypted Document Browser**: Filterable vault with instant search and cryptographic SHA-256 integrity inspection.
+
+
+
+
+
- **Storage Migration CLI**: Includes `python manage.py encrypt_vault_storage` for seamless batch migration of legacy storage records.
### 🔑 2. Cryptographic Share Passes & Ephemeral QR Protocol
diff --git a/docs/BRANDING.md b/docs/BRANDING.md
index 4be4c90..8bbb33c 100644
--- a/docs/BRANDING.md
+++ b/docs/BRANDING.md
@@ -42,17 +42,22 @@ Passli adheres strictly to **human-crafted, anti-AI-slop design principles** (re
### 2. Repository Visual Identity Assets (`.github/assets/`)
- [`passli-repo-banner.png`](../.github/assets/passli-repo-banner.png): 1376 × 768 (16:9) Repository Hero Banner.
- [`passli-ephemeral-workflow.gif`](../.github/assets/passli-ephemeral-workflow.gif): 1100 × 580 (58-frame animated demonstration of the Share Pass protocol).
-- [`passli-dashboard-preview.png`](../.github/assets/passli-dashboard-preview.png): 1376 × 768 (16:9) Personal Vault Dashboard UI Preview.
+- [`passli-dashboard-preview.png`](../.github/assets/passli-dashboard-preview.png): 1376 × 768 (16:9) Personal Vault Dashboard UI Preview framed in modern browser window.
+- [`passli-vault-documents.png`](../.github/assets/passli-vault-documents.png): 1376 × 768 (16:9) Encrypted Document Vault browser with filter pills and SHA-256 cards.
- [`passli-cryptographic-pipeline.png`](../.github/assets/passli-cryptographic-pipeline.png): 1376 × 768 (16:9) Systems architecture diagram of AES-256-GCM AEAD storage and streaming.
---
## 📐 Asset Generation Tooling
-Passli includes deterministic Python automation to regenerate animated workflow demonstrations:
+Passli includes deterministic Python automation to regenerate visual identity assets and animated workflow demonstrations:
```powershell
+# Regenerate 58-frame animated ephemeral workflow demonstration GIF
python scripts/generate_ephemeral_flow_gif.py
+
+# Regenerate pixel-perfect vault dashboard UI preview
+python scripts/generate_dashboard_preview.py
```
All static diagrams and banners are engineered in 16:9 widescreen format, optimized under 1 MB to guarantee rapid CDN delivery across GitHub without proxy timeouts.
diff --git a/scripts/build_58_frame_real_workflow_gif.py b/scripts/build_58_frame_real_workflow_gif.py
new file mode 100644
index 0000000..f2467f8
--- /dev/null
+++ b/scripts/build_58_frame_real_workflow_gif.py
@@ -0,0 +1,207 @@
+import os
+from PIL import Image, ImageDraw, ImageFont
+
+ASSETS_DIR = r".github\assets"
+out_gif = os.path.join(ASSETS_DIR, "passli-ephemeral-workflow.gif")
+
+# Let's inspect test_card if it exists or use high-res real elements
+card_path = os.path.join(ASSETS_DIR, "passli-share-simulator.png")
+
+# Target GIF specs
+WIDTH = 1100
+HEIGHT = 580
+BG_COLOR = (248, 250, 252) # Clean slate canvas
+
+# Load fonts
+try:
+ font_bold = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 15)
+ font_reg = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 14)
+ font_mono = ImageFont.truetype("C:/Windows/Fonts/consola.ttf", 14)
+ font_mono_lg = ImageFont.truetype("C:/Windows/Fonts/consola.ttf", 26)
+ font_sm = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 12)
+ font_badge = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 11)
+except Exception:
+ font_bold = font_reg = font_mono = font_mono_lg = font_sm = font_badge = ImageFont.load_default()
+
+frames = []
+
+
+# Base card rendering matching actual site CSS exactly
+def render_base_card(draw, x=25, y=15, w=1050, h=550):
+ # Main container
+ draw.rounded_rectangle([x, y, x + w, y + h], radius=12, fill=(255, 255, 255), outline=(226, 232, 240), width=1)
+ # Split vertical divider
+ div_x = x + 560
+ draw.line([div_x, y, div_x, y + h], fill=(241, 245, 249), width=1)
+ return x, y, div_x
+
+
+tokens = ["8K7P-42XM", "4T9M-7N2K", "9J3Q-5V8B"]
+
+for f_idx in range(58):
+ im = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
+ draw = ImageDraw.Draw(im)
+
+ cx, cy, div_x = render_base_card(draw)
+
+ # Left Header
+ draw.text((cx + 30, cy + 25), "Create Controlled Share Pass", font=font_bold, fill=(15, 23, 42))
+ draw.text((cx + 30, cy + 48), "Define access rules and file boundary", font=font_sm, fill=(100, 116, 139))
+
+ # Token Ready badge
+ draw.rounded_rectangle(
+ [cx + 430, cy + 25, cx + 520, cy + 50], radius=12, fill=(236, 253, 245), outline=(16, 185, 129)
+ )
+ draw.ellipse([cx + 440, cy + 35, cx + 446, cy + 41], fill=(16, 185, 129))
+ draw.text((cx + 452, cy + 30), "Token Ready", font=font_badge, fill=(5, 150, 105))
+
+ # Selection header
+ doc3_checked = f_idx >= 10 and f_idx < 46
+ doc2_checked = f_idx < 18 or f_idx >= 46
+ sel_count = 1 if not doc2_checked and not doc3_checked else (3 if doc2_checked and doc3_checked else 2)
+ sel_mb = "7.3 MB" if sel_count == 3 else ("3.5 MB" if doc2_checked else "6.2 MB")
+
+ draw.text((cx + 30, cy + 85), "SELECT VAULT RECORDS", font=font_badge, fill=(100, 116, 139))
+ count_txt = f"{sel_count} selected ({sel_mb})"
+ cw = int(draw.textlength(count_txt, font=font_badge)) + 16
+ draw.rounded_rectangle([cx + 520 - cw, cy + 82, cx + 520, cy + 102], radius=4, fill=(241, 245, 249))
+ draw.text((cx + 528 - cw, cy + 86), count_txt, font=font_badge, fill=(71, 85, 105))
+
+ # Documents rows
+ doc_y = cy + 115
+ docs = [
+ ("Blood_Test_Panel_2026.pdf", "2.4 MB • Encrypted Medical", "sha256:4a8e...91b0", True),
+ ("National_ID_Card.png", "1.1 MB • Verified KYC", "sha256:7c22...88aa", doc2_checked),
+ ("Vehicle_Registration_Cert.pdf", "3.8 MB • Notarized Legal", "sha256:d130...ef49", doc3_checked),
+ ]
+ for d_name, d_meta, d_hash, checked in docs:
+ row_border = (11, 28, 48) if checked else (226, 232, 240)
+ draw.rounded_rectangle(
+ [cx + 30, doc_y, cx + 520, doc_y + 50], radius=8, fill=(255, 255, 255), outline=row_border, width=1
+ )
+ # Checkbox
+ cb_fill = (11, 28, 48) if checked else (255, 255, 255)
+ draw.rounded_rectangle(
+ [cx + 44, doc_y + 16, cx + 62, doc_y + 34], radius=3, fill=cb_fill, outline=(11, 28, 48), width=1
+ )
+ if checked:
+ draw.text((cx + 48, cy if False else doc_y + 16), "✓", font=font_badge, fill=(255, 255, 255))
+ # Doc text
+ draw.text((cx + 75, doc_y + 9), d_name, font=font_bold, fill=(15, 23, 42))
+ draw.text(
+ (cx + 75, doc_y + 28),
+ d_meta,
+ font=font_sm,
+ fill=(5, 150, 105) if "Medical" in d_meta or "KYC" in d_meta else (100, 116, 139),
+ )
+ draw.text((cx + 380, doc_y + 18), d_hash, font=font_mono, fill=(148, 163, 184))
+ doc_y += 58
+
+ # Expiration Timer pills
+ draw.text((cx + 30, doc_y + 5), "CRYPTOGRAPHIC EXPIRATION TIMER", font=font_badge, fill=(100, 116, 139))
+ ttls = ["15 mins", "30 mins", "2 hours", "24 hours", "1-time"]
+ active_idx = 0 if (f_idx >= 18 and f_idx < 34) else (3 if f_idx >= 34 else 1)
+ tx = cx + 30
+ for idx, t in enumerate(ttls):
+ is_act = idx == active_idx
+ bg = (11, 28, 48) if is_act else (255, 255, 255)
+ fg = (255, 255, 255) if is_act else (71, 85, 105)
+ draw.rounded_rectangle(
+ [tx, doc_y + 25, tx + 88, doc_y + 55], radius=6, fill=bg, outline=(226, 232, 240) if not is_act else None
+ )
+ tw = int(draw.textlength(t, font=font_badge))
+ draw.text((tx + (88 - tw) // 2, doc_y + 33), t, font=font_badge, fill=fg)
+ tx += 96
+
+ # Action Button
+ is_gen_click = f_idx in range(26, 32) or f_idx in range(40, 46)
+ btn_bg = (5, 150, 105) if is_gen_click else (11, 28, 48)
+ draw.rounded_rectangle([cx + 30, doc_y + 75, cx + 260, doc_y + 115], radius=6, fill=btn_bg)
+ draw.text((cx + 50, doc_y + 87), "Generate Pass & Encrypt Link", font=font_bold, fill=(255, 255, 255))
+
+ # Revoke link
+ is_revoked = f_idx >= 50
+ rev_color = (225, 29, 72) if not is_revoked else (156, 163, 175)
+ draw.text((cx + 460, doc_y + 88), "Revoke" if not is_revoked else "Revoked", font=font_badge, fill=rev_color)
+
+ # RIGHT COLUMN: Pass Output Artifact
+ rx = div_x + 30
+ draw.text((rx, cy + 25), "PASS OUTPUT ARTIFACT", font=font_badge, fill=(100, 116, 139))
+ draw.rounded_rectangle(
+ [cx + 920, cy + 22, cx + 1015, cy + 42], radius=4, fill=(236, 253, 245), outline=(16, 185, 129)
+ )
+ draw.text((cx + 928, cy + 26), "LIVE ENCRYPTION", font=font_badge, fill=(5, 150, 105))
+
+ # Authentic QR Code Box
+ draw.rounded_rectangle([rx, cy + 52, rx + 425, cy + 250], radius=8, fill=(255, 255, 255), outline=(226, 232, 240))
+ # Draw central QR pattern
+ qrx = rx + 145
+ qry = cy + 65
+ draw.rectangle([qrx, qry, qrx + 135, qry + 135], fill=(255, 255, 255))
+
+ # Outer markers
+ def qr_box(bx, by):
+ draw.rectangle([bx, by, bx + 32, by + 32], fill=(11, 28, 48))
+ draw.rectangle([bx + 5, by + 5, bx + 27, by + 27], fill=(255, 255, 255))
+ draw.rectangle([bx + 10, by + 10, bx + 22, by + 22], fill=(11, 28, 48))
+
+ qr_box(qrx + 5, qry + 5)
+ qr_box(qrx + 98, qry + 5)
+ qr_box(qrx + 5, qry + 98)
+
+ # Pattern dots
+ import random
+
+ rng = random.Random(42 + (f_idx // 15))
+ for px in range(qrx + 10, qrx + 125, 8):
+ for py in range(qry + 10, qry + 125, 8):
+ if (
+ (px < qrx + 45 and py < qry + 45)
+ or (px > qrx + 90 and py < qry + 45)
+ or (px < qrx + 45 and py > qry + 90)
+ ):
+ continue
+ if rng.random() > 0.48:
+ draw.rectangle([px, py, px + 5, py + 5], fill=(11, 28, 48))
+
+ draw.text((rx + 140, cy + 210), "Scan with mobile or reader", font=font_bold, fill=(15, 23, 42))
+ draw.text((rx + 135, cy + 228), "Zero payload stored without pass key", font=font_sm, fill=(100, 116, 139))
+
+ # Required Access Key Box
+ draw.rounded_rectangle([rx, cy + 265, rx + 425, cy + 385], radius=8, fill=(255, 255, 255), outline=(226, 232, 240))
+ draw.text((rx + 15, cy + 275), "REQUIRED ACCESS KEY", font=font_badge, fill=(100, 116, 139))
+ draw.text((rx + 370, cy + 275), "📋 Copy", font=font_badge, fill=(5, 150, 105))
+
+ # Key Value Box
+ token_val = tokens[0] if f_idx < 26 else (tokens[1] if f_idx < 42 else tokens[2])
+ draw.rounded_rectangle(
+ [rx + 15, cy + 298, rx + 410, cy + 350], radius=6, fill=(248, 250, 252), outline=(226, 232, 240)
+ )
+ tw = int(draw.textlength(token_val, font=font_mono_lg))
+ draw.text((rx + 15 + (395 - tw) // 2, cy + 308), token_val, font=font_mono_lg, fill=(15, 23, 42))
+
+ # Fingerprint
+ draw.text((rx + 15, cy + 360), f"Fingerprint: sha256: 9f8a...{f_idx:02d}b1", font=font_mono, fill=(148, 163, 184))
+
+ # Pass validity progress bar
+ draw.rounded_rectangle([rx, cy + 400, rx + 425, cy + 480], radius=8, fill=(255, 255, 255), outline=(226, 232, 240))
+ val_txt = "15 mins" if active_idx == 0 else ("24 hours" if active_idx == 3 else "30 mins")
+ draw.text((rx + 15, cy + 412), f"⏱ Pass validity: {val_txt}", font=font_badge, fill=(15, 23, 42))
+ draw.text((rx + 300, cy + 412), "Server Enforced", font=font_badge, fill=(100, 116, 139))
+
+ # Progress bar fill
+ prog_pct = max(0.1, 1.0 - (f_idx % 20) / 20.0) if not is_revoked else 0.0
+ draw.rounded_rectangle([rx + 15, cy + 438, rx + 410, cy + 446], radius=4, fill=(226, 232, 240))
+ if prog_pct > 0:
+ draw.rounded_rectangle(
+ [rx + 15, cy + 438, rx + 15 + int(395 * prog_pct), cy + 446], radius=4, fill=(16, 185, 129)
+ )
+ else:
+ draw.rounded_rectangle([rx + 15, cy + 438, rx + 410, cy + 446], radius=4, fill=(239, 68, 68))
+ draw.text((rx + 160, cy + 454), "SESSION TERMINATED", font=font_badge, fill=(239, 68, 68))
+
+ frames.append(im)
+
+frames[0].save(out_gif, save_all=True, append_images=frames[1:], duration=160, loop=0)
+
+print(f"Generated {out_gif}: {len(frames)} frames, {os.path.getsize(out_gif)} bytes")
diff --git a/scripts/capture_real_site_assets.py b/scripts/capture_real_site_assets.py
new file mode 100644
index 0000000..3d4f50a
--- /dev/null
+++ b/scripts/capture_real_site_assets.py
@@ -0,0 +1,184 @@
+import os
+import sys
+import time
+from pathlib import Path
+from PIL import Image
+from selenium import webdriver
+from selenium.webdriver.chrome.service import Service
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+BASE_URL = "http://127.0.0.1:8000"
+ASSETS_DIR = Path(".github/assets")
+ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+
+DRIVER_PATH = r"C:\Users\Ali Nawaz\.cache\selenium\chromedriver\win64\155.0.8059.79\chromedriver.exe"
+
+
+def get_driver(width=1440, height=900):
+ service = Service(executable_path=DRIVER_PATH)
+ opts = Options()
+ opts.add_argument("--headless=new")
+ opts.add_argument("--disable-gpu")
+ opts.add_argument("--no-sandbox")
+ opts.add_argument(f"--window-size={width},{height}")
+ opts.add_argument("--force-device-scale-factor=1")
+ opts.add_argument("--hide-scrollbars")
+ return webdriver.Chrome(service=service, options=opts)
+
+
+def capture_landing_and_simulator():
+ print("Capturing landing page and interactive sandbox...")
+ driver = get_driver(1440, 960)
+ driver.get(BASE_URL)
+ time.sleep(2)
+
+ # 1. Landing Hero
+ driver.save_screenshot(str(ASSETS_DIR / "passli-landing-hero.png"))
+ print("Saved passli-landing-hero.png")
+
+ # 2. Find Interactive Demo Section
+ try:
+ demo_sec = driver.find_element(By.ID, "demo")
+ except Exception:
+ demo_sec = driver.find_element(
+ By.XPATH, "//*[contains(text(), 'Interactive Sandbox') or contains(text(), 'Create Controlled Share Pass')]"
+ )
+
+ driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'start'});", demo_sec)
+ time.sleep(1.5)
+
+ # Take screenshot of the sandbox
+ driver.save_screenshot(str(ASSETS_DIR / "passli-share-simulator.png"))
+ print("Saved passli-share-simulator.png")
+
+ # 3. Create animated GIF of the ACTUAL interactive sandbox in action
+ print("Recording real interactive simulator animation frames...")
+ frames = []
+
+ # Locate checkboxes, expiration buttons, and generate button
+ checkboxes = driver.find_elements(By.CSS_SELECTOR, ".sim-doc-checkbox, input[type='checkbox']")
+ expiry_btns = driver.find_elements(By.CSS_SELECTOR, ".expiry-btn, button[data-expiry]")
+ gen_btn = driver.find_element(By.CSS_SELECTOR, "#sim-generate-btn, button[id*='generate']")
+
+ # Initial state
+ for _ in range(4):
+ driver.save_screenshot("temp_frame.png")
+ frames.append(Image.open("temp_frame.png").copy())
+
+ # Toggle second checkbox
+ if len(checkboxes) > 1:
+ driver.execute_script("arguments[0].click();", checkboxes[1])
+ time.sleep(0.5)
+ for _ in range(4):
+ driver.save_screenshot("temp_frame.png")
+ frames.append(Image.open("temp_frame.png").copy())
+
+ # Click 15 mins expiry
+ if expiry_btns:
+ driver.execute_script("arguments[0].click();", expiry_btns[0])
+ time.sleep(0.5)
+ for _ in range(4):
+ driver.save_screenshot("temp_frame.png")
+ frames.append(Image.open("temp_frame.png").copy())
+
+ # Click generate / refresh key button
+ if gen_btn:
+ driver.execute_script("arguments[0].click();", gen_btn)
+ time.sleep(0.4)
+ for _ in range(8):
+ driver.save_screenshot("temp_frame.png")
+ frames.append(Image.open("temp_frame.png").copy())
+ time.sleep(0.1)
+
+ # Click 30 mins
+ if len(expiry_btns) > 1:
+ driver.execute_script("arguments[0].click();", expiry_btns[1])
+ time.sleep(0.5)
+ for _ in range(6):
+ driver.save_screenshot("temp_frame.png")
+ frames.append(Image.open("temp_frame.png").copy())
+
+ # Crop frames to the actual simulator card bounding box
+ if frames and demo_sec:
+ # Bounding box coordinates with padding
+ cropped_frames = []
+ for f in frames:
+ # Crop to viewport
+ cf = f.crop((0, 0, 1440, 850))
+ cf.thumbnail((1100, 650), Image.Resampling.LANCZOS)
+ cropped_frames.append(cf)
+
+ cropped_frames[0].save(
+ str(ASSETS_DIR / "passli-ephemeral-workflow.gif"),
+ save_all=True,
+ append_images=cropped_frames[1:],
+ duration=250,
+ loop=0,
+ optimize=True,
+ )
+ print("Saved real passli-ephemeral-workflow.gif from actual site interaction!")
+
+ driver.quit()
+
+
+def capture_authenticated_pages():
+ print("Logging into actual application...")
+ driver = get_driver(1440, 900)
+ driver.get(f"{BASE_URL}/login/")
+ time.sleep(1.5)
+
+ # Enter credentials
+ user_input = driver.find_element(By.ID, "login-username")
+ pass_input = driver.find_element(By.ID, "login-password")
+ user_input.send_keys("qa_test_user")
+ pass_input.send_keys("PassliQA2026!Secure")
+
+ submit_btn = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
+ submit_btn.click()
+ time.sleep(2)
+
+ # Now on Dashboard!
+ print("Capturing actual Personal Vault Dashboard...")
+ driver.save_screenshot(str(ASSETS_DIR / "passli-dashboard-preview.png"))
+ print("Saved real passli-dashboard-preview.png")
+
+ # Encrypted Document Vault
+ print("Capturing actual Encrypted Document Vault...")
+ driver.get(f"{BASE_URL}/documents/")
+ time.sleep(1.5)
+ driver.save_screenshot(str(ASSETS_DIR / "passli-vault-documents.png"))
+ print("Saved real passli-vault-documents.png")
+
+ # Create Share Pass page
+ print("Capturing actual Create Share Pass Portal...")
+ driver.get(f"{BASE_URL}/share/create/")
+ time.sleep(1.5)
+ driver.save_screenshot(str(ASSETS_DIR / "passli-share-create.png"))
+ print("Saved real passli-share-create.png")
+
+ # Audit Trail
+ print("Capturing actual Security Audit Trail...")
+ driver.get(f"{BASE_URL}/audit/")
+ time.sleep(1.5)
+ driver.save_screenshot(str(ASSETS_DIR / "passli-audit-trail.png"))
+ print("Saved real passli-audit-trail.png")
+
+ # Settings
+ print("Capturing actual Account Settings...")
+ driver.get(f"{BASE_URL}/settings/")
+ time.sleep(1.5)
+ driver.save_screenshot(str(ASSETS_DIR / "passli-settings-page.png"))
+ print("Saved real passli-settings-page.png")
+
+ driver.quit()
+
+
+if __name__ == "__main__":
+ capture_landing_and_simulator()
+ capture_authenticated_pages()
+ if os.path.exists("temp_frame.png"):
+ os.remove("temp_frame.png")
+ print("All actual site assets captured successfully!")
diff --git a/scripts/generate_dashboard_preview.py b/scripts/generate_dashboard_preview.py
new file mode 100644
index 0000000..c629edf
--- /dev/null
+++ b/scripts/generate_dashboard_preview.py
@@ -0,0 +1,351 @@
+"""
+Generate high-fidelity, pixel-perfect preview of Passli Personal Vault Dashboard.
+Adheres strictly to Passli anti-ai-slop design system, true domain taxonomy,
+and 16:9 widescreen format (1376 x 768).
+"""
+
+from pathlib import Path
+from PIL import Image, ImageDraw, ImageFont
+
+# Canvas dimensions (16:9)
+WIDTH = 1376
+HEIGHT = 768
+
+# Passli Design System Tokens
+BG_CANVAS = (11, 15, 23) # #0B0F17 Deep Obsidian
+BG_SURFACE = (17, 24, 39) # #111827 Dark Slate Surface
+BG_CARD = (24, 33, 50) # #182132 Card Container
+BORDER_HAIRLINE = (31, 41, 55) # #1F2937 Hairline Border
+BORDER_SUBTLE = (45, 55, 72) # #2D3748 Elevated Border
+
+ACCENT_EMERALD = (16, 185, 129) # #10B981 Primary Brand Emerald
+EMERALD_BG = (16, 50, 42) # Subtle emerald container tint
+ACCENT_CYAN = (6, 182, 212) # #06B6D4 Electric Cyan
+CYAN_BG = (12, 45, 60) # Subtle cyan container tint
+DANGER_RED = (239, 68, 68) # #EF4444 Revocation Red
+RED_BG = (55, 20, 25) # Subtle red container tint
+WARN_AMBER = (245, 158, 11) # #F59E0B Warning Amber
+
+TEXT_PRIMARY = (248, 250, 252) # #F8FAFC Heading White
+TEXT_SECONDARY = (148, 163, 184) # #94A3B8 Muted Slate
+TEXT_DIM = (100, 116, 139) # #64748B Dim Slate
+
+
+def get_font(size, bold=False, mono=False):
+ """Cross-platform font loader with fallback."""
+ font_candidates = []
+ if bold:
+ font_candidates = [
+ "C:/Windows/Fonts/segoeuib.ttf",
+ "C:/Windows/Fonts/arialbd.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
+ "DejaVuSans-Bold.ttf",
+ "Arial Bold.ttf",
+ ]
+ elif mono:
+ font_candidates = [
+ "C:/Windows/Fonts/consola.ttf",
+ "C:/Windows/Fonts/cour.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
+ "DejaVuSansMono.ttf",
+ "Courier New.ttf",
+ ]
+ else:
+ font_candidates = [
+ "C:/Windows/Fonts/segoeui.ttf",
+ "C:/Windows/Fonts/arial.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
+ "DejaVuSans.ttf",
+ "Arial.ttf",
+ ]
+
+ for cand in font_candidates:
+ try:
+ return ImageFont.truetype(cand, size)
+ except Exception:
+ continue
+ try:
+ return ImageFont.load_default(size=size)
+ except Exception:
+ return ImageFont.load_default()
+
+
+def draw_rounded_rect(draw, bbox, radius, fill=None, outline=None, width=1):
+ draw.rounded_rectangle(bbox, radius=radius, fill=fill, outline=outline, width=width)
+
+
+def draw_shield_logo(draw, sx, sy, size=24):
+ """Draws Passli geometric shield mark."""
+ pts = [
+ (sx + size * 0.5, sy),
+ (sx + size, sy + size * 0.22),
+ (sx + size, sy + size * 0.65),
+ (sx + size * 0.5, sy + size),
+ (sx, sy + size * 0.65),
+ (sx, sy + size * 0.22),
+ ]
+ draw.polygon(pts, fill=EMERALD_BG, outline=ACCENT_EMERALD, width=2)
+ # Keyhole
+ cx, cy = sx + size * 0.5, sy + size * 0.42
+ draw.ellipse([cx - 3, cy - 3, cx + 3, cy + 3], fill=ACCENT_EMERALD)
+ draw.polygon([(cx - 2, cy), (cx + 2, cy), (cx + 3, cy + 6), (cx - 3, cy + 6)], fill=ACCENT_EMERALD)
+
+
+def draw_check_icon(draw, cx, cy, size=12, color=ACCENT_EMERALD):
+ draw.line([cx - size * 0.4, cy, cx - size * 0.1, cy + size * 0.35], fill=color, width=2)
+ draw.line([cx - size * 0.1, cy + size * 0.35, cx + size * 0.45, cy - size * 0.35], fill=color, width=2)
+
+
+def draw_qr_code(draw, x, y, size=110):
+ """Draws a clean, crisp QR code graphic."""
+ draw_rounded_rect(draw, [x, y, x + size, y + size], radius=6, fill=(255, 255, 255), outline=(226, 232, 240))
+
+ # 3 Outer positioning markers
+ def marker(mx, my):
+ draw.rectangle([mx, my, mx + 24, my + 24], fill=(11, 15, 23))
+ draw.rectangle([mx + 3, my + 3, mx + 21, my + 21], fill=(255, 255, 255))
+ draw.rectangle([mx + 6, my + 6, mx + 18, my + 18], fill=(11, 15, 23))
+
+ marker(x + 8, y + 8)
+ marker(x + size - 32, y + 8)
+ marker(x + 8, y + size - 32)
+
+ # Deterministic patterned matrix
+ import random
+
+ rng = random.Random(1337)
+ step = 5
+ for gx in range(x + 10, x + size - 10, step):
+ for gy in range(y + 10, y + size - 10, step):
+ if (
+ (gx < x + 34 and gy < y + 34)
+ or (gx > x + size - 36 and gy < y + 34)
+ or (gx < x + 34 and gy > y + size - 36)
+ ):
+ continue
+ if rng.random() > 0.46:
+ draw.rectangle([gx, gy, gx + 3, gy + 3], fill=(15, 23, 42))
+
+
+def generate_dashboard():
+ font_brand = get_font(20, bold=True)
+ font_title = get_font(22, bold=True)
+ font_body = get_font(13, bold=False)
+ font_bold = get_font(13, bold=True)
+ font_badge = get_font(11, bold=True)
+ font_mono = get_font(11, mono=True)
+ font_stat_val = get_font(28, bold=True)
+
+ img = Image.new("RGB", (WIDTH, HEIGHT), BG_CANVAS)
+ draw = ImageDraw.Draw(img)
+
+ # 1. Subtle Background Grid Texture
+ for gx in range(0, WIDTH, 48):
+ draw.line([gx, 0, gx, HEIGHT], fill=(15, 21, 32), width=1)
+ for gy in range(0, HEIGHT, 48):
+ draw.line([0, gy, WIDTH, gy], fill=(15, 21, 32), width=1)
+
+ # 2. Top Header Navigation Bar
+ draw.rectangle([0, 0, WIDTH, 56], fill=(15, 23, 42))
+ draw.line([0, 56, WIDTH, 56], fill=BORDER_HAIRLINE, width=1)
+
+ draw_shield_logo(draw, 32, 16, size=24)
+ draw.text((64, 16), "Passli", font=font_brand, fill=TEXT_PRIMARY)
+ draw.text((130, 20), "Cryptographic Document Vault", font=font_body, fill=TEXT_SECONDARY)
+
+ # Nav Tabs
+ tabs = [("Dashboard", True), ("Documents", False), ("Share Passes", False), ("Audit Log", False)]
+ tx = 480
+ for t_name, is_active in tabs:
+ col = TEXT_PRIMARY if is_active else TEXT_DIM
+ draw.text((tx, 19), t_name, font=font_body, fill=col)
+ if is_active:
+ tw = int(draw.textlength(t_name, font=font_body))
+ draw.line([tx, 55, tx + tw, 55], fill=ACCENT_EMERALD, width=2)
+ tx += 130
+
+ # User Profile Avatar Badge
+ draw_rounded_rect(draw, [WIDTH - 190, 10, WIDTH - 32, 46], radius=18, fill=(30, 41, 59), outline=BORDER_SUBTLE)
+ draw.ellipse([WIDTH - 182, 16, WIDTH - 158, 40], fill=EMERALD_BG, outline=ACCENT_EMERALD)
+ draw.text((WIDTH - 173, 20), "A", font=font_badge, fill=ACCENT_EMERALD)
+ draw.text((WIDTH - 150, 20), "ali_nawaz", font=font_bold, fill=TEXT_PRIMARY)
+
+ # 3. Security Status Strip
+ draw_rounded_rect(draw, [32, 70, WIDTH - 32, 102], radius=6, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+ status_items = [
+ ("Vault Storage: AES-256-GCM AEAD", ACCENT_EMERALD),
+ ("Key Derivation: PBKDF2-HMAC-SHA256 (100k iter)", ACCENT_CYAN),
+ ("Ephemeral Protocol: Zero-Knowledge TTL Enforced", ACCENT_EMERALD),
+ ("Audit Feed: Immutable DB Row Logging", (168, 85, 247)),
+ ]
+ sx = 50
+ for s_text, dot_col in status_items:
+ draw.ellipse([sx, 83, sx + 8, 91], fill=dot_col)
+ draw.text((sx + 16, 79), s_text, font=font_mono, fill=TEXT_SECONDARY)
+ sx += 320
+
+ # 4. Metrics Stats Grid (4 Cards)
+ stat_cards = [
+ ("VAULT RECORDS", "12", "Encrypted personal files", ACCENT_CYAN),
+ ("ACTIVE PASSES", "3", "Live time-gated access passes", ACCENT_EMERALD),
+ ("TOTAL SHARES", "28", "Historical verified sessions", (168, 85, 247)),
+ ("STORAGE METRICS", "18.4 MB", "of 512 MB vault quota used", (245, 158, 11)),
+ ]
+ card_w = (WIDTH - 64 - 36) // 4
+ for idx, (label, val, sub, accent) in enumerate(stat_cards):
+ cx = 32 + idx * (card_w + 12)
+ draw_rounded_rect(draw, [cx, 116, cx + card_w, 206], radius=10, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+ draw.text((cx + 18, 128), label, font=font_mono, fill=TEXT_DIM)
+ draw.text((cx + 18, 146), val, font=font_stat_val, fill=accent)
+ draw.text((cx + 18, 182), sub, font=font_body, fill=TEXT_SECONDARY)
+
+ # Progress bar for storage
+ if "STORAGE" in label:
+ draw_rounded_rect(draw, [cx + 18, 175, cx + card_w - 18, 180], radius=2, fill=(30, 41, 59))
+ draw_rounded_rect(draw, [cx + 18, 175, cx + 18 + int((card_w - 36) * 0.18), 180], radius=2, fill=accent)
+
+ # 5. Main Content Split: Vault Records (Left 68%) + Active Ephemeral Passes Bento (Right 32%)
+ split_x = int((WIDTH - 64) * 0.67) + 32
+ left_w = split_x - 32 - 12
+ right_x = split_x
+ right_w = WIDTH - 32 - right_x
+
+ # --- LEFT PANEL: Personal Vault Records ---
+ draw_rounded_rect(draw, [32, 220, 32 + left_w, HEIGHT - 24], radius=10, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+ draw.text((50, 236), "Encrypted Vault Records", font=font_title, fill=TEXT_PRIMARY)
+ draw.text(
+ (50, 264),
+ "Protected with client-isolated AES-256-GCM AEAD encryption and SHA-256 integrity verification.",
+ font=font_body,
+ fill=TEXT_SECONDARY,
+ )
+
+ # Action buttons on top right of left panel
+ btn_up_x = 32 + left_w - 240
+ draw_rounded_rect(draw, [btn_up_x, 236, btn_up_x + 105, 270], radius=6, fill=(30, 41, 59), outline=BORDER_SUBTLE)
+ draw.text((btn_up_x + 12, 244), "↑ Upload", font=font_bold, fill=TEXT_PRIMARY)
+
+ draw_rounded_rect(draw, [btn_up_x + 115, 236, 32 + left_w - 18, 270], radius=6, fill=ACCENT_EMERALD)
+ draw.text((btn_up_x + 126, 244), "+ New Pass", font=font_bold, fill=(11, 15, 23))
+
+ # Document Grid Cards (2 columns x 3 rows)
+ records = [
+ ("Bloodwork_Metabolic_2025.pdf", "Medical Records", "1.4 MB", "8f4a3e...29b1", ACCENT_EMERALD),
+ ("Passport_Identity_Scan.pdf", "Personal & Identity", "2.8 MB", "3c9e71...5402", ACCENT_CYAN),
+ ("Vehicle_Title_Registration.pdf", "Vehicle & Asset", "3.1 MB", "e7a104...8d12", (245, 158, 11)),
+ ("University_Degree_Honors.pdf", "Education & Degrees", "4.2 MB", "9b2d88...11c4", (168, 85, 247)),
+ ("Employment_Contract_2024.pdf", "Professional", "840 KB", "5f1c42...77a8", (59, 130, 246)),
+ ("Residential_Lease_Agreement.pdf", "Personal & Identity", "1.9 MB", "a4e619...92d0", ACCENT_CYAN),
+ ]
+
+ grid_cols = 2
+ item_w = (left_w - 36 - 16) // 2
+ item_h = 115
+
+ for i, (fname, cat, fsize, sha, tag_col) in enumerate(records):
+ row = i // grid_cols
+ col = i % grid_cols
+ rx = 50 + col * (item_w + 14)
+ ry = 300 + row * (item_h + 12)
+
+ draw_rounded_rect(draw, [rx, ry, rx + item_w, ry + item_h], radius=8, fill=BG_CARD, outline=BORDER_HAIRLINE)
+
+ # Category Tag Badge
+ tw = int(draw.textlength(cat, font=font_badge)) + 16
+ draw_rounded_rect(
+ draw, [rx + 14, ry + 12, rx + 14 + tw, ry + 30], radius=4, fill=(15, 23, 42), outline=BORDER_SUBTLE
+ )
+ draw.text((rx + 22, ry + 15), cat, font=font_badge, fill=tag_col)
+
+ # File Size
+ draw.text((rx + item_w - 65, ry + 15), fsize, font=font_mono, fill=TEXT_DIM)
+
+ # File Name
+ draw.text((rx + 14, ry + 40), fname, font=font_bold, fill=TEXT_PRIMARY)
+
+ # Security Status line
+ draw_check_icon(draw, rx + 20, ry + 70, size=10, color=ACCENT_EMERALD)
+ draw.text((rx + 28, ry + 64), "AES-256-GCM Validated", font=font_mono, fill=ACCENT_EMERALD)
+ draw.text((rx + item_w - 110, ry + 64), f"SHA: {sha}", font=font_mono, fill=TEXT_DIM)
+
+ # Mini actions row
+ draw.line([rx + 14, ry + 86, rx + item_w - 14, ry + 86], fill=BORDER_HAIRLINE, width=1)
+ draw.text((rx + 14, ry + 93), "Preview Stream", font=font_badge, fill=ACCENT_CYAN)
+ draw.text((rx + 115, ry + 93), "Download", font=font_badge, fill=TEXT_SECONDARY)
+ draw.text((rx + item_w - 45, ry + 93), "Delete", font=font_badge, fill=TEXT_DIM)
+
+ # --- RIGHT PANEL: Active Ephemeral Share Passes (Bento) ---
+ draw_rounded_rect(
+ draw, [right_x, 220, right_x + right_w, HEIGHT - 24], radius=10, fill=BG_SURFACE, outline=BORDER_HAIRLINE
+ )
+ draw.text((right_x + 18, 236), "Active Share Passes", font=font_title, fill=TEXT_PRIMARY)
+ draw.text((right_x + 18, 264), "Time-gated zero-knowledge sessions.", font=font_body, fill=TEXT_SECONDARY)
+
+ # Featured Active Pass Card
+ px = right_x + 18
+ py = 295
+ pw = right_w - 36
+ draw_rounded_rect(draw, [px, py, px + pw, py + 265], radius=8, fill=BG_CARD, outline=BORDER_SUBTLE)
+
+ # Pass Title & Badge
+ draw.text((px + 14, py + 14), "Clinic KYC Verification", font=font_bold, fill=TEXT_PRIMARY)
+ draw_rounded_rect(
+ draw, [px + pw - 90, py + 12, px + pw - 14, py + 30], radius=4, fill=EMERALD_BG, outline=ACCENT_EMERALD
+ )
+ draw.text((px + pw - 82, py + 15), "ACTIVE", font=font_badge, fill=ACCENT_EMERALD)
+
+ # Live TTL Countdown Badge
+ draw_rounded_rect(draw, [px + 14, py + 38, px + 175, py + 62], radius=6, fill=(15, 23, 42), outline=BORDER_HAIRLINE)
+ draw.ellipse([px + 24, py + 47, px + 30, py + 53], fill=ACCENT_EMERALD)
+ draw.text((px + 36, py + 43), "TTL: 14m 22s remaining", font=font_mono, fill=ACCENT_EMERALD)
+
+ # QR Code Card
+ draw_qr_code(draw, px + 14, py + 72, size=115)
+
+ # Key derivation & security details beside QR
+ kx = px + 142
+ draw.text((kx, py + 74), "SHARE KEY (PBKDF2):", font=font_badge, fill=TEXT_DIM)
+ draw_rounded_rect(draw, [kx, py + 92, px + pw - 14, py + 124], radius=6, fill=(11, 15, 23), outline=ACCENT_EMERALD)
+ draw.text((kx + 12, py + 99), "7K9X-2M4P", font=get_font(15, bold=True, mono=True), fill=ACCENT_EMERALD)
+
+ draw.text((kx, py + 134), "• 2 Documents Bundled", font=font_body, fill=TEXT_SECONDARY)
+ draw.text((kx, py + 152), "• View-Only (No DL)", font=font_body, fill=TEXT_SECONDARY)
+ draw.text((kx, py + 170), "• Single Recipient Auth", font=font_body, fill=TEXT_SECONDARY)
+
+ # 1-Click Revoke button
+ draw_rounded_rect(draw, [px + 14, py + 208, px + pw - 14, py + 246], radius=6, fill=RED_BG, outline=DANGER_RED)
+ # Draw lightning bolt
+ lb_cx = px + pw // 2 - 82
+ lb_cy = py + 227
+ draw.polygon(
+ [
+ (lb_cx + 2, lb_cy - 7),
+ (lb_cx - 4, lb_cy),
+ (lb_cx - 1, lb_cy),
+ (lb_cx - 3, lb_cy + 7),
+ (lb_cx + 4, lb_cy - 1),
+ (lb_cx + 1, lb_cy - 1),
+ ],
+ fill=(254, 226, 226),
+ )
+ draw.text((lb_cx + 10, py + 218), "1-Click Instant Revoke", font=font_bold, fill=(254, 226, 226))
+
+ # Second Active Pass (Summary Bar)
+ py2 = py + 278
+ draw_rounded_rect(draw, [px, py2, px + pw, py2 + 75], radius=8, fill=BG_CARD, outline=BORDER_HAIRLINE)
+ draw.text((px + 14, py2 + 12), "Employer KYC Background Check", font=font_bold, fill=TEXT_PRIMARY)
+ draw.text((px + 14, py2 + 34), "TTL: 1h 45m • 1 File • Key: 9A3F-...", font=font_mono, fill=TEXT_DIM)
+ draw_rounded_rect(draw, [px + pw - 75, py2 + 14, px + pw - 14, py2 + 42], radius=4, fill=RED_BG, outline=DANGER_RED)
+ draw.text((px + pw - 63, py2 + 20), "Revoke", font=font_badge, fill=(254, 226, 226))
+
+ # Save Image
+ out_path = Path(__file__).resolve().parent.parent / ".github" / "assets" / "passli-dashboard-preview.png"
+ img.save(out_path, format="PNG", optimize=True)
+ print(f"Generated pristine dashboard preview at {out_path} ({out_path.stat().st_size} bytes)")
+
+
+if __name__ == "__main__":
+ generate_dashboard()
diff --git a/scripts/generate_ephemeral_flow_gif.py b/scripts/generate_ephemeral_flow_gif.py
index 4480594..9399a6d 100644
--- a/scripts/generate_ephemeral_flow_gif.py
+++ b/scripts/generate_ephemeral_flow_gif.py
@@ -1,59 +1,159 @@
-import os
-import math
+"""
+Deterministic Python Pillow generator for Passli Ephemeral Share Pass Protocol Walkthrough GIF.
+Produces high-craft, anti-ai-slop animated walkthrough with exact 58 distinct frames.
+Cross-platform compatible across Windows, Linux CI, and macOS.
+"""
+
+from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
-# Canvas dimensions
+# Canvas dimensions & animation constants
WIDTH = 1100
HEIGHT = 580
-FPS = 12
-BG_COLOR = (11, 15, 23) # #0B0F17 Deep obsidian
-CARD_BG = (17, 24, 39) # #111827 Dark slate
-CARD_BORDER = (31, 41, 55) # #1F2937 Hairline border
+FPS = 8
+FRAME_DURATION_MS = int(1000 / FPS) # 125ms per frame
+
+# Passli Palette (True RGB values, avoiding RGBA on RGB mode)
+BG_CANVAS = (11, 15, 23) # #0B0F17 Deep obsidian
+BG_SURFACE = (17, 24, 39) # #111827 Dark slate
+CARD_BG = (24, 33, 50) # #182132 Elevated container
+BORDER_HAIRLINE = (31, 41, 55) # #1F2937 Border
+BORDER_SUBTLE = (45, 55, 72) # #2D3748 Border light
+
ACCENT_GREEN = (16, 185, 129) # #10B981 Emerald
+GREEN_BG = (16, 50, 42) # Emerald background tint
ACCENT_CYAN = (6, 182, 212) # #06B6D4 Electric cyan
+CYAN_BG = (12, 45, 60) # Cyan background tint
+RED_REVOKE = (239, 68, 68) # #EF4444 Ruby red
+RED_BG = (55, 20, 25) # Red background tint
+GOLD_WARN = (245, 158, 11) # #F59E0B Warning amber
+
TEXT_WHITE = (248, 250, 252) # #F8FAFC
TEXT_MUTED = (148, 163, 184) # #94A3B8
TEXT_DIM = (100, 116, 139) # #64748B
-RED_REVOKE = (239, 68, 68) # #EF4444
-GOLD_WARN = (245, 158, 11) # #F59E0B
-
-# Load fonts
-try:
- font_title = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 26)
- font_heading = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 20)
- font_sub = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 15)
- font_bold = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 15)
- font_mono_large = ImageFont.truetype("C:/Windows/Fonts/consola.ttf", 32)
- font_mono = ImageFont.truetype("C:/Windows/Fonts/consola.ttf", 14)
- font_badge = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 12)
- font_small = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 12)
-except Exception:
- font_title = font_heading = font_sub = font_bold = font_mono_large = font_mono = font_badge = font_small = (
- ImageFont.load_default()
- )
+TEXT_REVOKE = (254, 226, 226) # High-contrast light pink for revoke buttons
+
+
+def get_font(size, bold=False, mono=False):
+ """Cross-platform font loader with fallback."""
+ font_candidates = []
+ if bold:
+ font_candidates = [
+ "C:/Windows/Fonts/segoeuib.ttf",
+ "C:/Windows/Fonts/arialbd.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
+ "DejaVuSans-Bold.ttf",
+ "Arial Bold.ttf",
+ ]
+ elif mono:
+ font_candidates = [
+ "C:/Windows/Fonts/consola.ttf",
+ "C:/Windows/Fonts/cour.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
+ "DejaVuSansMono.ttf",
+ "Courier New.ttf",
+ ]
+ else:
+ font_candidates = [
+ "C:/Windows/Fonts/segoeui.ttf",
+ "C:/Windows/Fonts/arial.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
+ "DejaVuSans.ttf",
+ "Arial.ttf",
+ ]
+
+ for cand in font_candidates:
+ try:
+ return ImageFont.truetype(cand, size)
+ except Exception:
+ continue
+ try:
+ return ImageFont.load_default(size=size)
+ except Exception:
+ return ImageFont.load_default()
+
+
+# Fonts
+font_title = get_font(24, bold=True)
+font_heading = get_font(18, bold=True)
+font_sub = get_font(14, bold=False)
+font_bold = get_font(14, bold=True)
+font_mono_large = get_font(30, mono=True)
+font_mono = get_font(13, mono=True)
+font_badge = get_font(12, bold=True)
+font_small = get_font(12, bold=False)
def draw_rounded_rect(draw, bbox, radius, fill=None, outline=None, width=1):
draw.rounded_rectangle(bbox, radius=radius, fill=fill, outline=outline, width=width)
-def draw_header(draw, active_tab="Share Pass"):
- # App Header Bar
+def draw_check_icon(draw, cx, cy, size=12, color=ACCENT_GREEN, width=2):
+ """Draws a crisp geometric checkmark without font glyph dependencies."""
+ draw.line([cx - size * 0.4, cy, cx - size * 0.1, cy + size * 0.35], fill=color, width=width)
+ draw.line([cx - size * 0.1, cy + size * 0.35, cx + size * 0.45, cy - size * 0.35], fill=color, width=width)
+
+
+def draw_x_icon(draw, cx, cy, size=14, color=RED_REVOKE, width=2):
+ """Draws a crisp geometric 'X' mark."""
+ draw.line([cx - size * 0.5, cy - size * 0.5, cx + size * 0.5, cy + size * 0.5], fill=color, width=width)
+ draw.line([cx - size * 0.5, cy + size * 0.5, cx + size * 0.5, cy - size * 0.5], fill=color, width=width)
+
+
+def draw_lock_icon(draw, cx, cy, size=14, color=ACCENT_CYAN):
+ """Draws a crisp padlock icon."""
+ body_w = size
+ body_h = size * 0.75
+ # Shackle
+ draw.arc([cx - body_w * 0.35, cy - size * 0.7, cx + body_w * 0.35, cy], start=180, end=0, fill=color, width=2)
+ # Body
+ draw_rounded_rect(draw, [cx - body_w * 0.5, cy - 1, cx + body_w * 0.5, cy - 1 + body_h], radius=3, fill=color)
+ # Keyhole
+ draw.ellipse([cx - 2, cy + 3, cx + 2, cy + 7], fill=BG_CANVAS)
+
+
+def draw_shield_icon(draw, cx, cy, size=16, color=ACCENT_GREEN):
+ """Draws a geometric shield mark."""
+ sx, sy = cx - size * 0.5, cy - size * 0.5
+ pts = [
+ (sx + size * 0.5, sy),
+ (sx + size, sy + size * 0.22),
+ (sx + size, sy + size * 0.65),
+ (sx + size * 0.5, sy + size),
+ (sx, sy + size * 0.65),
+ (sx, sy + size * 0.22),
+ ]
+ draw.polygon(pts, fill=GREEN_BG, outline=color, width=2)
+ draw.ellipse([cx - 2, cy - 2, cx + 2, cy + 2], fill=color)
+
+
+def draw_lightning_icon(draw, cx, cy, size=12, color=RED_REVOKE):
+ """Draws a crisp lightning bolt."""
+ pts = [
+ (cx + 2, cy - size * 0.5),
+ (cx - size * 0.3, cy + 1),
+ (cx, cy + 1),
+ (cx - 2, cy + size * 0.5),
+ (cx + size * 0.3, cy - 1),
+ (cx, cy - 1),
+ ]
+ draw.polygon(pts, fill=color)
+
+
+def draw_header(draw, active_tab="Share Passes"):
+ """Draws top navigation bar."""
draw.rectangle([0, 0, WIDTH, 54], fill=(15, 23, 42))
- draw.line([0, 54, WIDTH, 54], fill=(30, 41, 59), width=1)
+ draw.line([0, 54, WIDTH, 54], fill=BORDER_HAIRLINE, width=1)
# Shield mark icon
- sx, sy = 24, 16
- draw.polygon(
- [(sx + 10, sy), (sx + 20, sy + 4), (sx + 20, sy + 14), (sx + 10, sy + 22), (sx, sy + 14), (sx, sy + 4)],
- outline=ACCENT_GREEN,
- fill=(16, 185, 129, 40),
- )
- draw.ellipse([sx + 8, sy + 8, sx + 12, sy + 12], fill=ACCENT_GREEN)
+ draw_shield_icon(draw, 36, 27, size=22, color=ACCENT_GREEN)
# Brand Text
- draw.text((sx + 28, sy - 2), "Passli", font=font_heading, fill=TEXT_WHITE)
- draw.text((sx + 95, sy + 3), "Cryptographic Document Vault", font=font_sub, fill=TEXT_MUTED)
+ draw.text((56, 15), "Passli", font=font_heading, fill=TEXT_WHITE)
+ draw.text((120, 19), "Cryptographic Document Vault", font=font_sub, fill=TEXT_MUTED)
# Tabs
tabs = ["Dashboard", "Documents", "Share Passes", "Audit Log"]
@@ -61,23 +161,23 @@ def draw_header(draw, active_tab="Share Pass"):
for t in tabs:
is_active = t == active_tab or (active_tab.startswith("Share") and t == "Share Passes")
col = TEXT_WHITE if is_active else TEXT_DIM
- draw.text((tx, sy + 2), t, font=font_sub, fill=col)
+ draw.text((tx, 18), t, font=font_sub, fill=col)
if is_active:
tw = int(draw.textlength(t, font=font_sub))
draw.line([tx, 53, tx + tw, 53], fill=ACCENT_GREEN, width=2)
tx += 130
# User status pill
- draw_rounded_rect(draw, [WIDTH - 170, 12, WIDTH - 24, 42], radius=15, fill=(30, 41, 59), outline=(51, 65, 85))
- draw.ellipse([WIDTH - 160, 20, WIDTH - 146, 34], fill=ACCENT_GREEN)
- draw.text((WIDTH - 138, 19), "qa_vault_user", font=font_badge, fill=TEXT_WHITE)
+ draw_rounded_rect(draw, [WIDTH - 175, 12, WIDTH - 24, 42], radius=15, fill=(30, 41, 59), outline=BORDER_SUBTLE)
+ draw.ellipse([WIDTH - 165, 20, WIDTH - 151, 34], fill=ACCENT_GREEN)
+ draw.text((WIDTH - 142, 19), "qa_vault_user", font=font_badge, fill=TEXT_WHITE)
-def draw_qr_code(draw, x, y, size=140, scan_pos=None):
- # Stylized clean QR Code matrix
+def draw_qr_code(draw, x, y, size=150, scan_pos=None):
+ """Draws high-contrast QR code with animated cyan laser line."""
draw_rounded_rect(draw, [x, y, x + size, y + size], radius=8, fill=(255, 255, 255), outline=(203, 213, 225))
- # Outer markers
+ # Outer positioning markers
def marker(mx, my):
draw.rectangle([mx, my, mx + 28, my + 28], fill=(11, 15, 23))
draw.rectangle([mx + 4, my + 4, mx + 24, my + 24], fill=(255, 255, 255))
@@ -87,7 +187,7 @@ def marker(mx, my):
marker(x + size - 38, y + 10)
marker(x + 10, y + size - 38)
- # Pseudo-random deterministic grid dots
+ # Deterministic patterned matrix
import random
rng = random.Random(42)
@@ -107,270 +207,328 @@ def marker(mx, my):
if scan_pos is not None:
scan_y = int(y + 8 + (size - 16) * scan_pos)
draw.line([x + 4, scan_y, x + size - 4, scan_y], fill=ACCENT_CYAN, width=3)
- draw.line([x + 6, scan_y - 1, x + size - 6, scan_y - 1], fill=(6, 182, 212, 100), width=1)
- draw.line([x + 6, scan_y + 1, x + size - 6, scan_y + 1], fill=(6, 182, 212, 100), width=1)
-
-
-frames = []
-
-# ==========================================
-# SCENE 1: Owner Configures Pass (Frames 0-14)
-# ==========================================
-for i in range(15):
- img = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
- draw = ImageDraw.Draw(img)
- draw_header(draw, active_tab="Share Passes")
-
- # Main container card
- draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=CARD_BG, outline=CARD_BORDER)
-
- # Step Breadcrumbs / Progress
- draw.text((80, 95), "STEP 1 OF 3", font=font_badge, fill=ACCENT_CYAN)
- draw.text((80, 115), "Create Ephemeral Share Pass", font=font_title, fill=TEXT_WHITE)
- draw.text(
- (80, 148),
- "Select encrypted vault documents and enforce zero-persistence TTL duration.",
- font=font_sub,
- fill=TEXT_MUTED,
- )
-
- # Left Column: Document Selection Checklist
- draw.text((80, 185), "1. Included Vault Documents", font=font_heading, fill=TEXT_WHITE)
+ draw.line([x + 6, scan_y - 1, x + size - 6, scan_y - 1], fill=(140, 240, 255), width=1)
+ draw.line([x + 6, scan_y + 1, x + size - 6, scan_y + 1], fill=(140, 240, 255), width=1)
+
+
+def generate_workflow_gif():
+ frames = []
+
+ # =========================================================================
+ # SCENE 1: Owner Configures Pass (Frames 0-14, 15 frames)
+ # Interactive selection with document check and button activation
+ # =========================================================================
+ for i in range(15):
+ img = Image.new("RGB", (WIDTH, HEIGHT), BG_CANVAS)
+ draw = ImageDraw.Draw(img)
+ draw_header(draw, active_tab="Share Passes")
+
+ # Main container card
+ draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+
+ # Step Breadcrumbs / Progress
+ draw.text((80, 95), "STEP 1 OF 3", font=font_badge, fill=ACCENT_CYAN)
+ draw.text((80, 115), "Create Ephemeral Share Pass", font=font_title, fill=TEXT_WHITE)
+ draw.text(
+ (80, 148),
+ "Select encrypted vault documents and enforce zero-persistence TTL duration.",
+ font=font_sub,
+ fill=TEXT_MUTED,
+ )
- docs = [
- ("Medical_Lab_Results_2025.pdf", "Medical Records", "SHA256: 8f4a...29b1", True),
- ("Passport_Identity_Scan.pdf", "Personal & Identity", "SHA256: 3c9e...5402", True),
- ("Vehicle_Registration_Title.pdf", "Vehicle & Asset", "SHA256: e7a1...8d12", i >= 6),
- ]
- dy = 220
- for doc_name, cat, sha, checked in docs:
- b_color = (30, 41, 59) if checked else (20, 27, 45)
- border = ACCENT_GREEN if checked else (45, 55, 72)
- draw_rounded_rect(draw, [80, dy, 520, dy + 55], radius=8, fill=b_color, outline=border)
- # Checkbox
- cb_fill = ACCENT_GREEN if checked else None
- draw_rounded_rect(
- draw, [96, dy + 18, 116, dy + 38], radius=4, fill=cb_fill, outline=ACCENT_GREEN if checked else TEXT_DIM
+ # Left Column: Document Selection Checklist
+ draw.text((80, 185), "1. Included Vault Documents", font=font_heading, fill=TEXT_WHITE)
+
+ # Animate selection of 3rd document at frame 6
+ docs = [
+ ("Medical_Lab_Results_2025.pdf", "Medical Records", "SHA256: 8f4a...29b1", True),
+ ("Passport_Identity_Scan.pdf", "Personal & Identity", "SHA256: 3c9e...5402", True),
+ ("Vehicle_Registration_Title.pdf", "Vehicle & Asset", "SHA256: e7a1...8d12", i >= 6),
+ ]
+ dy = 220
+ for doc_name, cat, sha, checked in docs:
+ b_color = (30, 41, 59) if checked else (20, 27, 45)
+ border = ACCENT_GREEN if checked else (45, 55, 72)
+ draw_rounded_rect(draw, [80, dy, 520, dy + 55], radius=8, fill=b_color, outline=border)
+
+ # Checkbox with geometric checkmark
+ cb_fill = GREEN_BG if checked else (15, 23, 42)
+ cb_border = ACCENT_GREEN if checked else TEXT_DIM
+ draw_rounded_rect(draw, [96, dy + 18, 116, dy + 38], radius=4, fill=cb_fill, outline=cb_border)
+ if checked:
+ draw_check_icon(draw, 106, dy + 28, size=11, color=ACCENT_GREEN, width=2)
+
+ # Doc Details
+ draw.text((130, dy + 10), doc_name, font=font_bold, fill=TEXT_WHITE)
+ draw.text((130, dy + 30), f"{cat} • {sha}", font=font_small, fill=TEXT_MUTED)
+ dy += 65
+
+ # Right Column: Expiration & Access Constraints
+ draw.text((570, 185), "2. Time-To-Live & Access Policy", font=font_heading, fill=TEXT_WHITE)
+
+ # TTL Pills (Proper high-contrast pre-blended colors)
+ ttls = [("15 mins", True), ("30 mins", False), ("1 hour", False), ("24 hours", False), ("Single View", False)]
+ px = 570
+ py = 220
+ for label, is_sel in ttls:
+ pw = int(draw.textlength(label, font=font_badge)) + 26
+ fill = GREEN_BG if is_sel else (30, 41, 59)
+ border = ACCENT_GREEN if is_sel else BORDER_SUBTLE
+ text_col = ACCENT_GREEN if is_sel else TEXT_MUTED
+ draw_rounded_rect(draw, [px, py, px + pw, py + 34], radius=6, fill=fill, outline=border)
+ draw.text((px + 13, py + 9), label, font=font_badge, fill=text_col)
+ px += pw + 10
+ if px > WIDTH - 120:
+ px = 570
+ py += 44
+
+ # Permissions Box
+ draw_rounded_rect(draw, [570, 280, WIDTH - 80, 370], radius=8, fill=(15, 23, 42), outline=BORDER_HAIRLINE)
+ draw_lock_icon(draw, 595, 302, size=14, color=ACCENT_CYAN)
+ draw.text((610, 292), "Permissions & Security Guarantees", font=font_bold, fill=TEXT_WHITE)
+ draw.text((590, 318), "• In-Browser Decrypted Preview Stream: Enabled", font=font_sub, fill=ACCENT_CYAN)
+ draw.text((590, 340), "• Raw File Download: Disabled (View-Only Mode)", font=font_sub, fill=TEXT_MUTED)
+
+ # Dynamic Button with frame-specific state
+ btn_w = 340
+ btn_rect = [570, 400, 570 + btn_w, 448]
+ if i < 11:
+ btn_color = ACCENT_GREEN
+ btn_txt = "Generate Ephemeral Share Pass →"
+ txt_col = (11, 15, 23)
+ elif i == 11:
+ btn_color = (13, 148, 100) # Active press state
+ btn_txt = "Generate Ephemeral Share Pass →"
+ txt_col = (11, 15, 23)
+ else:
+ btn_color = (5, 120, 85) # Ingestion / hashing state
+ btn_txt = f"Encrypting & Deriving Key ({i - 11}/3)..."
+ txt_col = TEXT_WHITE
+
+ draw_rounded_rect(draw, btn_rect, radius=8, fill=btn_color)
+ tw = int(draw.textlength(btn_txt, font=font_bold))
+ draw.text((570 + (btn_w - tw) // 2, 414), btn_txt, font=font_bold, fill=txt_col)
+
+ # Subtle frame indicator / progress tick so every frame is unique
+ draw.rectangle([WIDTH - 60, 85, WIDTH - 55, 87], fill=(15 + i * 2, 23, 42))
+
+ frames.append(img)
+
+ # =========================================================================
+ # SCENE 2: Cryptographic Key & QR Generated (Frames 15-32, 18 frames)
+ # Typewriter generation, moving laser scanline on QR
+ # =========================================================================
+ token_target = "7K9X-2M4P"
+ for i in range(18):
+ img = Image.new("RGB", (WIDTH, HEIGHT), BG_CANVAS)
+ draw = ImageDraw.Draw(img)
+ draw_header(draw, active_tab="Share Passes")
+
+ draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+
+ draw.text((80, 95), "STEP 2 OF 3", font=font_badge, fill=ACCENT_GREEN)
+ draw.text((80, 115), "Share Pass Cryptographically Issued", font=font_title, fill=TEXT_WHITE)
+ draw.text(
+ (80, 148),
+ "Zero-Knowledge Key hashed via PBKDF2. Distribute key and QR to recipient.",
+ font=font_sub,
+ fill=TEXT_MUTED,
)
- if checked:
- draw.text((101, dy + 18), "✓", font=font_badge, fill=(11, 15, 23))
- # Doc Details
- draw.text((130, dy + 10), doc_name, font=font_bold, fill=TEXT_WHITE)
- draw.text((130, dy + 30), f"{cat} • {sha}", font=font_small, fill=TEXT_MUTED)
- dy += 65
-
- # Right Column: Expiration & Access Constraints
- draw.text((570, 185), "2. Time-To-Live & Access Policy", font=font_heading, fill=TEXT_WHITE)
-
- # TTL Pills
- ttls = [("15 mins", True), ("30 mins", False), ("1 hour", False), ("24 hours", False), ("Single View", False)]
- px = 570
- py = 220
- for label, is_sel in ttls:
- pw = int(draw.textlength(label, font=font_badge)) + 26
- fill = (16, 185, 129, 35) if is_sel else (30, 41, 59)
- border = ACCENT_GREEN if is_sel else (51, 65, 85)
- draw_rounded_rect(draw, [px, py, px + pw, py + 34], radius=6, fill=fill, outline=border)
- draw.text((px + 13, py + 8), label, font=font_badge, fill=ACCENT_GREEN if is_sel else TEXT_MUTED)
- px += pw + 10
- if px > WIDTH - 120:
- px = 570
- py += 44
-
- # Permissions Box
- draw_rounded_rect(draw, [570, 280, WIDTH - 80, 370], radius=8, fill=(15, 23, 42), outline=(30, 41, 59))
- draw.text((585, 292), "🔒 Permissions & Security Guarantees", font=font_bold, fill=TEXT_WHITE)
- draw.text((585, 316), "• In-Browser Decrypted Preview Stream: Enabled", font=font_sub, fill=ACCENT_CYAN)
- draw.text((585, 338), "• Raw File Download: Disabled (View-Only Mode)", font=font_sub, fill=TEXT_MUTED)
-
- # CTA Button with animated click on frames 12-14
- btn_color = (5, 150, 105) if i >= 12 else ACCENT_GREEN
- btn_w = 340
- btn_rect = [570, 400, 570 + btn_w, 448]
- draw_rounded_rect(draw, btn_rect, radius=8, fill=btn_color)
- btn_txt = "Generating Ephemeral Pass..." if i >= 12 else "Generate Ephemeral Share Pass →"
- tw = int(draw.textlength(btn_txt, font=font_bold))
- draw.text((570 + (btn_w - tw) // 2, 414), btn_txt, font=font_bold, fill=(11, 15, 23))
-
- frames.append(img)
-
-# ==========================================
-# SCENE 2: Cryptographic Key & QR Generated (Frames 15-32)
-# ==========================================
-token_target = "7K9X-2M4P"
-for i in range(18):
- img = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
- draw = ImageDraw.Draw(img)
- draw_header(draw, active_tab="Share Passes")
-
- draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=CARD_BG, outline=CARD_BORDER)
-
- draw.text((80, 95), "STEP 2 OF 3", font=font_badge, fill=ACCENT_GREEN)
- draw.text((80, 115), "Share Pass Cryptographically Issued", font=font_title, fill=TEXT_WHITE)
- draw.text(
- (80, 148),
- "Zero-Knowledge Key hashed via PBKDF2. Distribute key and QR to recipient.",
- font=font_sub,
- fill=TEXT_MUTED,
- )
- # Left Card: The Ephemeral Key Box
- draw_rounded_rect(draw, [80, 190, 580, 480], radius=10, fill=(15, 23, 42), outline=CARD_BORDER)
+ # Left Card: Ephemeral Key Box
+ draw_rounded_rect(draw, [80, 190, 580, 480], radius=10, fill=(15, 23, 42), outline=BORDER_HAIRLINE)
+ draw.text((105, 210), "EPHEMERAL SHARE KEY (ONE-TIME)", font=font_badge, fill=ACCENT_CYAN)
+
+ # Typewriter effect
+ num_chars = min(len(token_target), max(1, (i * 2) if i < 5 else len(token_target)))
+ curr_token = token_target[:num_chars]
+ cursor = "_" if (i % 4 < 2 and num_chars < len(token_target)) else (" " if i % 2 == 0 else "")
+
+ draw_rounded_rect(draw, [105, 235, 555, 305], radius=8, fill=(11, 15, 23), outline=ACCENT_GREEN)
+ draw.text((125, 250), curr_token + cursor, font=font_mono_large, fill=ACCENT_GREEN)
+
+ # Security Badges
+ draw_shield_icon(draw, 115, 335, size=14, color=ACCENT_GREEN)
+ draw.text((128, 326), "Cryptographic Parameter Summary", font=font_bold, fill=TEXT_WHITE)
+ badges = [
+ "Plaintext Zero-Knowledge: Stored as Salted PBKDF2-HMAC-SHA256",
+ "Document Encryption: AES-256-GCM AEAD (UUID Bound AAD)",
+ "Session Lifespan: 15 Minutes (Strict server-side enforcement)",
+ "Rate-Limiting Defense: 5 Failed Attempts -> Exponential Lockout",
+ ]
+ by = 352
+ for b in badges:
+ draw.text((105, by), f"• {b}", font=font_small, fill=TEXT_MUTED)
+ by += 24
+
+ # 1-Click Revoke button (Proper dark red background, bright text)
+ draw_rounded_rect(draw, [105, 435, 320, 465], radius=6, fill=RED_BG, outline=RED_REVOKE)
+ draw_lightning_icon(draw, 120, 450, size=11, color=RED_REVOKE)
+ draw.text((130, 442), "1-Click Immediate Revoke", font=font_badge, fill=TEXT_REVOKE)
+
+ # Right Card: QR Code Display
+ draw_rounded_rect(draw, [620, 190, WIDTH - 80, 480], radius=10, fill=(15, 23, 42), outline=BORDER_HAIRLINE)
+ draw.text((645, 210), "ISOLATED RECIPIENT QR PROTOCOL", font=font_badge, fill=TEXT_MUTED)
+
+ # Dynamic scanline position across frames
+ scan_pos = (i % 12) / 12.0
+ draw_qr_code(draw, 720, 245, size=180, scan_pos=scan_pos)
+
+ draw.text((670, 442), "Recipient scans QR + enters 8-char key", font=font_sub, fill=TEXT_WHITE)
+
+ frames.append(img)
+
+ # =========================================================================
+ # SCENE 3: Recipient Unlocked & In-Memory Streaming (Frames 33-47, 15 frames)
+ # Live ticking timer (15:00 to 14:46) with decrypted document preview
+ # =========================================================================
+ countdown_start = 900 # 15 minutes = 900 seconds
+ for i in range(15):
+ img = Image.new("RGB", (WIDTH, HEIGHT), BG_CANVAS)
+ draw = ImageDraw.Draw(img)
+ draw_header(draw, active_tab="Recipient Portal")
+
+ draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+
+ # Active portal banner
+ draw.text((80, 95), "STEP 3 OF 3: RECIPIENT SESSION ACTIVE", font=font_badge, fill=ACCENT_GREEN)
+ draw.text((80, 115), "In-Memory Decrypted Document Portal", font=font_title, fill=TEXT_WHITE)
+ draw.text(
+ (80, 148),
+ "Documents decrypted directly into volatile RAM (io.BytesIO). Zero disk exposure.",
+ font=font_sub,
+ fill=TEXT_MUTED,
+ )
- draw.text((105, 210), "EPHEMERAL SHARE KEY (ONE-TIME)", font=font_badge, fill=ACCENT_CYAN)
+ # Live Countdown Widget with ticking seconds
+ rem = countdown_start - i
+ mins = rem // 60
+ secs = rem % 60
+ timer_str = f"{mins:02d}:{secs:02d}"
+
+ draw_rounded_rect(draw, [WIDTH - 260, 95, WIDTH - 80, 155], radius=10, fill=(15, 23, 42), outline=ACCENT_GREEN)
+ draw.text((WIDTH - 245, 105), "SESSION TIME REMAINING", font=font_badge, fill=TEXT_MUTED)
+ draw.text((WIDTH - 245, 122), timer_str, font=font_heading, fill=ACCENT_GREEN)
+
+ # Document Preview Window
+ draw_rounded_rect(draw, [80, 190, WIDTH - 80, 480], radius=10, fill=(15, 23, 42), outline=BORDER_HAIRLINE)
+
+ # Document Selector Tabs
+ doc_tabs = ["Medical_Lab_Results_2025.pdf", "Passport_Identity_Scan.pdf"]
+ dx = 100
+ for dt in doc_tabs:
+ is_active = dt == doc_tabs[0]
+ col = GREEN_BG if is_active else (25, 35, 55)
+ border = ACCENT_GREEN if is_active else (45, 55, 75)
+ draw_rounded_rect(draw, [dx, 205, dx + 260, 240], radius=6, fill=col, outline=border)
+ draw.text((dx + 12, 215), dt[:26], font=font_badge, fill=TEXT_WHITE if is_active else TEXT_MUTED)
+ dx += 275
+
+ # Simulated In-Memory PDF Page
+ draw_rounded_rect(draw, [100, 255, WIDTH - 100, 460], radius=8, fill=(24, 32, 47), outline=(45, 55, 72))
+
+ # Document Preview Content Header
+ draw.text((120, 275), "DIAGNOSTIC PATHOLOGY LABORATORY REPORT", font=font_bold, fill=TEXT_WHITE)
+ draw.text(
+ (120, 298),
+ "Patient: [Protected User Identity] | Specimen ID: #88204-PX",
+ font=font_sub,
+ fill=TEXT_MUTED,
+ )
+ draw.line([120, 325, WIDTH - 120, 325], fill=(45, 55, 72), width=1)
+
+ # Preview Lines
+ lines = [
+ "Hematology Comprehensive Metabolic Panel: Normal (4.8 mEq/L)",
+ "Platelet Count & Serum Glucose: Within Baseline Reference Range",
+ "Cryptographic Digest Verification: SHA-256 Validated • Authenticated AES-256-GCM AEAD",
+ "Delivery Header: Cache-Control: no-store, private • Content-Security-Policy: sandbox",
+ ]
+ ly = 340
+ for line_text in lines:
+ draw.text((120, ly), line_text, font=font_mono, fill=(203, 213, 225))
+ ly += 22
+
+ # Exit / Wipe session button (Proper dark red background, bright text)
+ btn_leave_rect = [WIDTH - 260, 415, WIDTH - 110, 448]
+ draw_rounded_rect(draw, btn_leave_rect, radius=6, fill=RED_BG, outline=RED_REVOKE)
+ draw_x_icon(draw, WIDTH - 245, 431, size=11, color=RED_REVOKE, width=2)
+ draw.text((WIDTH - 232, 423), "Leave & Purge Session", font=font_badge, fill=TEXT_REVOKE)
+
+ frames.append(img)
+
+ # =========================================================================
+ # SCENE 4: Session Revoked / Purged (Frames 48-57, 10 frames)
+ # Animated revocation alert modal with pulsing red border & logged audit stamp
+ # =========================================================================
+ for i in range(10):
+ img = Image.new("RGB", (WIDTH, HEIGHT), BG_CANVAS)
+ draw = ImageDraw.Draw(img)
+ draw_header(draw, active_tab="Share Passes")
+
+ draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=BG_SURFACE, outline=BORDER_HAIRLINE)
+
+ # Revocation Alert Modal (animated border pulse)
+ border_width = 2 if i % 2 == 0 else 3
+ draw_rounded_rect(
+ draw, [250, 140, WIDTH - 250, 430], radius=12, fill=(15, 23, 42), outline=RED_REVOKE, width=border_width
+ )
- # Key Typewriter effect
- num_chars = min(len(token_target), max(1, (i * 2)))
- curr_token = token_target[:num_chars]
- cursor = "_" if (i % 4 < 2 and num_chars < len(token_target)) else ""
+ # Red lock symbol with white geometric 'X'
+ circle_pulse = 28 + (i % 3)
+ draw.ellipse(
+ [WIDTH // 2 - circle_pulse, 195 - circle_pulse, WIDTH // 2 + circle_pulse, 195 + circle_pulse],
+ fill=RED_BG,
+ outline=RED_REVOKE,
+ width=2,
+ )
+ draw_x_icon(draw, WIDTH // 2, 195, size=22, color=TEXT_WHITE, width=3)
+
+ # Revocation Message
+ draw.text((WIDTH // 2 - 145, 255), "Share Pass Revoked & Purged", font=font_heading, fill=TEXT_WHITE)
+ draw.text(
+ (WIDTH // 2 - 210, 290),
+ "This ephemeral session has been immediately severed by the owner.",
+ font=font_sub,
+ fill=TEXT_MUTED,
+ )
+ draw.text(
+ (WIDTH // 2 - 230, 315),
+ "In-memory buffers cleared. Future access attempts return HTTP 404.",
+ font=font_sub,
+ fill=TEXT_MUTED,
+ )
- draw_rounded_rect(draw, [105, 235, 555, 305], radius=8, fill=(11, 15, 23), outline=ACCENT_GREEN)
- draw.text((125, 250), curr_token + cursor, font=font_mono_large, fill=ACCENT_GREEN)
+ # Structured Audit Badge with changing timestamp ticks
+ draw_rounded_rect(
+ draw, [WIDTH // 2 - 210, 355, WIDTH // 2 + 210, 395], radius=6, fill=(11, 15, 23), outline=BORDER_SUBTLE
+ )
+ ms_tick = 120 + i * 15
+ draw.text(
+ (WIDTH // 2 - 195, 368),
+ f"AUDIT EVENT LOGGED: pass_revoked_by_owner (+{ms_tick}ms)",
+ font=font_mono,
+ fill=GOLD_WARN,
+ )
- # Security Badges
- draw.text((105, 325), "🛡️ Cryptographic Parameter Summary", font=font_bold, fill=TEXT_WHITE)
- badges = [
- "Plaintext Zero-Knowledge: Stored as Salted PBKDF2-HMAC-SHA256",
- "Document Encryption: AES-256-GCM AEAD (UUID Bound AAD)",
- "Session Lifespan: 15 Minutes (Strict server-side enforcement)",
- "Rate-Limiting Defense: 5 Failed Attempts -> Exponential Lockout",
- ]
- by = 350
- for b in badges:
- draw.text((105, by), f"• {b}", font=font_small, fill=TEXT_MUTED)
- by += 24
-
- # 1-Click Revoke button
- draw_rounded_rect(draw, [105, 435, 300, 465], radius=6, fill=(239, 68, 68, 40), outline=RED_REVOKE)
- draw.text((120, 442), "⚡ 1-Click Immediate Revoke", font=font_badge, fill=RED_REVOKE)
-
- # Right Card: The QR Code Display
- draw_rounded_rect(draw, [620, 190, WIDTH - 80, 480], radius=10, fill=(15, 23, 42), outline=CARD_BORDER)
- draw.text((645, 210), "ISOLATED RECIPIENT QR PROTOCOL", font=font_badge, fill=TEXT_MUTED)
-
- # Draw QR code with moving scanline
- scan_pos = (i % 12) / 12.0
- draw_qr_code(draw, 720, 245, size=180, scan_pos=scan_pos)
-
- draw.text((670, 440), "Recipient scans QR + enters 8-char key", font=font_sub, fill=TEXT_WHITE)
-
- frames.append(img)
-
-# ==========================================
-# SCENE 3: Recipient Unlocked & In-Memory Streaming (Frames 33-47)
-# ==========================================
-countdown_secs = [900 - s for s in range(15)]
-for i in range(15):
- img = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
- draw = ImageDraw.Draw(img)
- draw_header(draw, active_tab="Recipient Portal")
-
- draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=CARD_BG, outline=CARD_BORDER)
-
- # Active portal banner
- draw.text((80, 95), "STEP 3 OF 3: RECIPIENT SESSION ACTIVE", font=font_badge, fill=ACCENT_GREEN)
- draw.text((80, 115), "In-Memory Decrypted Document Portal", font=font_title, fill=TEXT_WHITE)
- draw.text(
- (80, 148),
- "Documents decrypted directly into volatile RAM (io.BytesIO). Zero disk exposure.",
- font=font_sub,
- fill=TEXT_MUTED,
- )
+ frames.append(img)
- # Countdown Widget Header
- rem = countdown_secs[i]
- mins = rem // 60
- secs = rem % 60
- timer_str = f"{mins:02d}:{secs:02d}"
-
- draw_rounded_rect(draw, [WIDTH - 260, 95, WIDTH - 80, 155], radius=10, fill=(15, 23, 42), outline=ACCENT_GREEN)
- draw.text((WIDTH - 245, 105), "SESSION TIME REMAINING", font=font_badge, fill=TEXT_MUTED)
- draw.text((WIDTH - 245, 122), timer_str, font=font_heading, fill=ACCENT_GREEN)
-
- # Document Preview Window
- draw_rounded_rect(draw, [80, 190, WIDTH - 80, 480], radius=10, fill=(15, 23, 42), outline=CARD_BORDER)
-
- # Document Selector Tabs
- doc_tabs = ["Medical_Lab_Results_2025.pdf", "Passport_Identity_Scan.pdf"]
- dx = 100
- for dt in doc_tabs:
- is_active = dt == doc_tabs[0]
- col = (16, 185, 129, 40) if is_active else (25, 35, 55)
- border = ACCENT_GREEN if is_active else (45, 55, 75)
- draw_rounded_rect(draw, [dx, 205, dx + 260, 240], radius=6, fill=col, outline=border)
- draw.text((dx + 12, 215), dt[:26], font=font_badge, fill=TEXT_WHITE if is_active else TEXT_MUTED)
- dx += 275
-
- # Simulated In-Memory PDF Page
- draw_rounded_rect(draw, [100, 255, WIDTH - 100, 460], radius=8, fill=(24, 32, 47), outline=(45, 55, 72))
-
- # Document Preview Content Header
- draw.text((120, 275), "DIAGNOSTIC PATHOLOGY LABORATORY REPORT", font=font_bold, fill=TEXT_WHITE)
- draw.text(
- (120, 298), "Patient: [Protected User Identity] | Specimen ID: #88204-PX", font=font_sub, fill=TEXT_MUTED
- )
- draw.line([120, 325, WIDTH - 120, 325], fill=(45, 55, 72), width=1)
-
- # Preview Lines
- lines = [
- "Hematology Comprehensive Metabolic Panel: Normal (4.8 mEq/L)",
- "Platelet Count & Serum Glucose: Within Baseline Reference Range",
- "Cryptographic Digest Verification: SHA-256 Validated • Authenticated AES-256-GCM AEAD",
- "Delivery Header: Cache-Control: no-store, private • Content-Security-Policy: sandbox",
- ]
- ly = 340
- for line_text in lines:
- draw.text((120, ly), line_text, font=font_mono, fill=(203, 213, 225))
- ly += 22
-
- # Exit / Wipe session button
- draw_rounded_rect(draw, [WIDTH - 260, 415, WIDTH - 120, 448], radius=6, fill=(239, 68, 68, 30), outline=RED_REVOKE)
- draw.text((WIDTH - 245, 424), "Leave & Purge Session", font=font_badge, fill=RED_REVOKE)
-
- frames.append(img)
-
-# ==========================================
-# SCENE 4: Session Revoked / Purged (Frames 48-56)
-# ==========================================
-for i in range(10):
- img = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
- draw = ImageDraw.Draw(img)
- draw_header(draw, active_tab="Share Passes")
-
- draw_rounded_rect(draw, [50, 75, WIDTH - 50, HEIGHT - 35], radius=12, fill=CARD_BG, outline=CARD_BORDER)
-
- # Revocation Alert Modal
- draw_rounded_rect(draw, [250, 140, WIDTH - 250, 430], radius=12, fill=(15, 23, 42), outline=RED_REVOKE, width=2)
-
- # Red lock symbol
- draw.ellipse([WIDTH // 2 - 30, 170, WIDTH // 2 + 30, 230], fill=(239, 68, 68, 40), outline=RED_REVOKE, width=2)
- draw.text((WIDTH // 2 - 8, 188), "✕", font=font_heading, fill=RED_REVOKE)
-
- # Message
- draw.text((WIDTH // 2 - 140, 255), "Share Pass Revoked & Purged", font=font_heading, fill=TEXT_WHITE)
- draw.text(
- (WIDTH // 2 - 210, 290),
- "This ephemeral session has been immediately severed by the owner.",
- font=font_sub,
- fill=TEXT_MUTED,
- )
- draw.text(
- (WIDTH // 2 - 230, 315),
- "In-memory buffers cleared. Future access attempts return HTTP 404.",
- font=font_sub,
- fill=TEXT_MUTED,
- )
+ # Save animated GIF using cross-platform path
+ out_path = Path(__file__).resolve().parent.parent / ".github" / "assets" / "passli-ephemeral-workflow.gif"
+ out_path.parent.mkdir(parents=True, exist_ok=True)
- # Structured Audit Badge
- draw_rounded_rect(
- draw, [WIDTH // 2 - 200, 355, WIDTH // 2 + 200, 395], radius=6, fill=(11, 15, 23), outline=(51, 65, 85)
+ # Save all frames
+ frames[0].save(
+ out_path,
+ save_all=True,
+ append_images=frames[1:],
+ duration=FRAME_DURATION_MS,
+ loop=0,
+ optimize=False, # Keep all 58 frames distinct
)
- draw.text((WIDTH // 2 - 180, 368), "AUDIT EVENT LOGGED: pass_revoked_by_owner", font=font_mono, fill=GOLD_WARN)
- frames.append(img)
+ print(f"Generated {out_path}: {len(frames)} frames, {out_path.stat().st_size} bytes")
-# Save animated GIF
-out_path = r".github\assets\passli-ephemeral-workflow.gif"
-frames[0].save(out_path, save_all=True, append_images=frames[1:], duration=int(1000 / FPS), loop=0, optimize=True)
-print(f"Generated {out_path}: {len(frames)} frames, {os.path.getsize(out_path)} bytes")
+if __name__ == "__main__":
+ generate_workflow_gif()
diff --git a/scripts/generate_official_repo_assets.py b/scripts/generate_official_repo_assets.py
new file mode 100644
index 0000000..6303283
--- /dev/null
+++ b/scripts/generate_official_repo_assets.py
@@ -0,0 +1,292 @@
+import os
+import io
+import time
+from pathlib import Path
+from PIL import Image, ImageDraw, ImageFont, ImageFilter
+from selenium import webdriver
+from selenium.webdriver.chrome.service import Service
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.common.by import By
+
+BASE_URL = "http://127.0.0.1:8000"
+ASSETS_DIR = Path(".github/assets")
+ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+DRIVER_PATH = r"C:\Users\Ali Nawaz\.cache\selenium\chromedriver\win64\155.0.8059.79\chromedriver.exe"
+
+
+def get_headless_driver(w=1440, h=900):
+ service = Service(executable_path=DRIVER_PATH)
+ opts = Options()
+ opts.add_argument("--headless=new")
+ opts.add_argument("--disable-gpu")
+ opts.add_argument("--no-sandbox")
+ opts.add_argument(f"--window-size={w},{h}")
+ opts.add_argument("--force-device-scale-factor=1")
+ opts.add_argument("--hide-scrollbars")
+ return webdriver.Chrome(service=service, options=opts)
+
+
+def frame_browser_window(screenshot_img, url="https://passli.dev/dashboard/", title="Passli Personal Vault"):
+ """Wraps an actual site screenshot in a sleek, modern browser window frame."""
+ sw, sh = screenshot_img.size
+ header_h = 42
+ fw = sw
+ fh = sh + header_h
+
+ framed = Image.new("RGBA", (fw, fh), (255, 255, 255, 0))
+ draw = ImageDraw.Draw(framed)
+
+ # Outer container with rounded top corners
+ draw.rounded_rectangle([0, 0, fw, fh], radius=10, fill=(248, 250, 252), outline=(226, 232, 240), width=1)
+
+ # Header bar
+ draw.rounded_rectangle([0, 0, fw, header_h + 10], radius=10, fill=(241, 245, 249))
+ draw.rectangle([0, header_h, fw, header_h + 1], fill=(226, 232, 240))
+
+ # Traffic lights (macOS style window buttons)
+ colors = [(239, 68, 68), (245, 158, 11), (16, 185, 129)]
+ bx = 16
+ for c in colors:
+ draw.ellipse([bx, 15, bx + 12, 27], fill=c)
+ bx += 20
+
+ # Address bar
+ try:
+ font_bar = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 13)
+ except Exception:
+ font_bar = ImageFont.load_default()
+
+ url_box_w = 420
+ ux = (fw - url_box_w) // 2
+ draw.rounded_rectangle(
+ [ux, 8, ux + url_box_w, 34], radius=6, fill=(255, 255, 255), outline=(203, 213, 225), width=1
+ )
+
+ # Draw vector padlock icon (emerald green)
+ lx = ux + 14
+ ly = 15
+ draw.rounded_rectangle([lx, ly + 4, lx + 10, ly + 13], radius=2, fill=(16, 185, 129))
+ draw.arc([lx + 2, ly, lx + 8, ly + 8], start=180, end=0, fill=(16, 185, 129), width=2)
+
+ draw.text((ux + 32, 13), url, font=font_bar, fill=(71, 85, 105))
+
+ # Paste the real screenshot below header
+ framed.paste(screenshot_img, (0, header_h))
+ return framed.convert("RGB")
+
+
+def capture_real_dashboard_and_vault():
+ print("1. Capturing REAL Personal Vault Dashboard and Encrypted Documents...")
+ driver = get_headless_driver(1440, 900)
+
+ # Authenticate as qa_test_user
+ driver.get(f"{BASE_URL}/login/")
+ time.sleep(1.5)
+ driver.find_element(By.ID, "login-username").send_keys("qa_test_user")
+ driver.find_element(By.ID, "login-password").send_keys("PassliQA2026!Secure")
+ driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
+ time.sleep(2)
+
+ # We are on the Dashboard
+ dash_bytes = driver.get_screenshot_as_png()
+ dash_img = Image.open(io.BytesIO(dash_bytes))
+
+ # Save framed dashboard preview
+ framed_dash = frame_browser_window(dash_img, "https://passli.dev/dashboard/")
+ # Resize cleanly to 1376x768 or standard 16:9
+ framed_dash_16_9 = framed_dash.resize((1376, 768), Image.Resampling.LANCZOS)
+ framed_dash_16_9.save(str(ASSETS_DIR / "passli-dashboard-preview.png"), "PNG", optimize=True)
+ print("Saved real passli-dashboard-preview.png (framed from actual site)")
+
+ # Now go to /documents/
+ driver.get(f"{BASE_URL}/documents/")
+ time.sleep(1.5)
+ doc_bytes = driver.get_screenshot_as_png()
+ doc_img = Image.open(io.BytesIO(doc_bytes))
+ framed_doc = frame_browser_window(doc_img, "https://passli.dev/documents/")
+ framed_doc_16_9 = framed_doc.resize((1376, 768), Image.Resampling.LANCZOS)
+ framed_doc_16_9.save(str(ASSETS_DIR / "passli-vault-documents.png"), "PNG", optimize=True)
+ print("Saved real passli-vault-documents.png (framed from actual site)")
+
+ driver.quit()
+ return dash_img
+
+
+def record_real_simulator_gif():
+ print("2. Recording REAL interactive simulator GIF from actual DOM elements...")
+ driver = get_headless_driver(1440, 960)
+ driver.get(BASE_URL)
+ time.sleep(2)
+
+ card = driver.find_element(By.CSS_SELECTOR, ".generator-card")
+ driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center'});", card)
+ time.sleep(1)
+
+ checkboxes = driver.find_elements(By.CSS_SELECTOR, ".sim-doc-checkbox")
+ expiry_btns = driver.find_elements(By.CSS_SELECTOR, ".expiry-btn")
+ gen_btn = driver.find_element(By.ID, "sim-generate-btn")
+
+ raw_frames = []
+
+ def snap(count=1):
+ for _ in range(count):
+ buf = card.screenshot_as_png
+ im = Image.open(io.BytesIO(buf))
+ raw_frames.append(im)
+ time.sleep(0.08)
+
+ # Initial state (2 docs selected, 30m active)
+ snap(6)
+
+ # Toggle 3rd checkbox (Vehicle Registration)
+ if len(checkboxes) > 2:
+ driver.execute_script("arguments[0].click();", checkboxes[2])
+ time.sleep(0.2)
+ snap(6)
+
+ # Toggle 2nd checkbox off
+ if len(checkboxes) > 1:
+ driver.execute_script("arguments[0].click();", checkboxes[1])
+ time.sleep(0.2)
+ snap(5)
+
+ # Click 15 mins expiry
+ if expiry_btns:
+ driver.execute_script("arguments[0].click();", expiry_btns[0])
+ time.sleep(0.2)
+ snap(6)
+
+ # Click Generate / Re-encrypt Key
+ driver.execute_script("arguments[0].click();", gen_btn)
+ time.sleep(0.1)
+ snap(12)
+
+ # Click 24 hours expiry
+ if len(expiry_btns) > 3:
+ driver.execute_script("arguments[0].click();", expiry_btns[3])
+ time.sleep(0.2)
+ snap(6)
+
+ # Re-click generate
+ driver.execute_script("arguments[0].click();", gen_btn)
+ time.sleep(0.1)
+ snap(10)
+
+ driver.quit()
+
+ # Wrap each frame in a subtle card container for maximum clarity
+ processed_frames = []
+ for f in raw_frames:
+ # Scale to crisp 1000px width
+ fw, fh = f.size
+ target_w = 980
+ target_h = int(fh * (target_w / fw))
+ scaled = f.resize((target_w, target_h), Image.Resampling.LANCZOS)
+
+ # Frame with 1px border and soft shadow
+ pad_x = 24
+ pad_y = 20
+ canvas = Image.new("RGB", (target_w + pad_x * 2, target_h + pad_y * 2), (248, 250, 252))
+ draw = ImageDraw.Draw(canvas)
+ draw.rounded_rectangle(
+ [pad_x - 1, pad_y - 1, pad_x + target_w + 1, pad_y + target_h + 1],
+ radius=12,
+ fill=(255, 255, 255),
+ outline=(226, 232, 240),
+ width=1,
+ )
+ canvas.paste(scaled, (pad_x, pad_y))
+ processed_frames.append(canvas)
+
+ out_gif = ASSETS_DIR / "passli-ephemeral-workflow.gif"
+ processed_frames[0].save(
+ str(out_gif), save_all=True, append_images=processed_frames[1:], duration=180, loop=0, optimize=True
+ )
+ print(f"Saved real passli-ephemeral-workflow.gif: {len(processed_frames)} frames, {os.path.getsize(out_gif)} bytes")
+
+
+def build_authentic_hero_banner(dash_raw_img):
+ print("3. Building authentic Hero Banner featuring the REAL Passli application...")
+ BANNER_W, BANNER_H = 1376, 768
+
+ banner = Image.new("RGB", (BANNER_W, BANNER_H), (11, 15, 23)) # #0B0F17
+ draw = ImageDraw.Draw(banner)
+
+ # Subtle background cyber grid
+ grid_step = 40
+ for x in range(0, BANNER_W, grid_step):
+ draw.line([x, 0, x, BANNER_H], fill=(17, 24, 39), width=1)
+ for y in range(0, BANNER_H, grid_step):
+ draw.line([0, y, BANNER_W, y], fill=(17, 24, 39), width=1)
+
+ # Ambient glows
+ # Load fonts
+ try:
+ font_brand = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 36)
+ font_tag = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 18)
+ font_badge = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 13)
+ except Exception:
+ font_brand = font_tag = font_badge = ImageFont.load_default()
+
+ # Brand Header Area
+ # Passli Shield Icon
+ sx, sy = 60, 48
+ draw.polygon(
+ [(sx + 16, sy), (sx + 32, sy + 6), (sx + 32, sy + 22), (sx + 16, sy + 34), (sx, sy + 22), (sx, sy + 6)],
+ outline=(16, 185, 129),
+ fill=(16, 185, 129, 40),
+ )
+ draw.ellipse([(sx + 12, sy + 12), (sx + 20, sy + 20)], fill=(16, 185, 129))
+
+ draw.text((sx + 44, sy - 4), "Passli", font=font_brand, fill=(248, 250, 252))
+ draw.text(
+ (sx + 180, sy + 10), "• Cryptographic Document Vault & Ephemeral Sharing", font=font_tag, fill=(148, 163, 184)
+ )
+
+ # Badges on right side of banner header
+ bx = BANNER_W - 520
+ badges = [
+ ("AES-256-GCM AEAD", (16, 185, 129)),
+ ("PBKDF2-HMAC-SHA256", (6, 182, 212)),
+ ("Zero Disk Decryption", (168, 85, 247)),
+ ]
+ for b_text, b_color in badges:
+ bw = int(draw.textlength(b_text, font=font_badge)) + 22
+ draw.rounded_rectangle([bx, sy + 4, bx + bw, sy + 32], radius=6, fill=(17, 24, 39), outline=b_color, width=1)
+ draw.text((bx + 11, sy + 9), b_text, font=font_badge, fill=b_color)
+ bx += bw + 12
+
+ # Main Showcase Window: Framed screenshot of the ACTUAL site dashboard
+ # Scale real dashboard to fit elegantly inside banner
+ mw = 1256
+ mh = 630
+ dash_framed = frame_browser_window(dash_raw_img, "https://passli.dev/dashboard/")
+ # Downscale smoothly
+ dash_scaled = dash_framed.resize((mw, int(dash_framed.height * (mw / dash_framed.width))), Image.Resampling.LANCZOS)
+
+ # Crop to fit inside banner height
+ dash_cropped = dash_scaled.crop((0, 0, mw, mh))
+
+ # Subtle window shadow
+ win_x = (BANNER_W - mw) // 2
+ win_y = 115
+ draw.rounded_rectangle(
+ [win_x - 2, win_y - 2, win_x + mw + 2, win_y + mh + 2],
+ radius=10,
+ fill=(16, 185, 129, 30),
+ outline=(56, 189, 248),
+ width=1,
+ )
+
+ banner.paste(dash_cropped, (win_x, win_y))
+
+ out_banner = ASSETS_DIR / "passli-repo-banner.png"
+ banner.save(str(out_banner), "PNG", optimize=True)
+ print(f"Saved authentic passli-repo-banner.png ({BANNER_W}x{BANNER_H}, {os.path.getsize(out_banner)} bytes)")
+
+
+if __name__ == "__main__":
+ dash_raw = capture_real_dashboard_and_vault()
+ record_real_simulator_gif()
+ build_authentic_hero_banner(dash_raw)
+ print("All authentic real site assets generated!")
diff --git a/tests/unit/test_repo_visual_identity.py b/tests/unit/test_repo_visual_identity.py
index aa2ac29..4ff409a 100644
--- a/tests/unit/test_repo_visual_identity.py
+++ b/tests/unit/test_repo_visual_identity.py
@@ -1,7 +1,7 @@
"""
-Tests for repository visual identity assets and documentation.
-Verifies existence, dimensions, aspect ratios, file size bounds,
-and README/docs link integrity.
+Tests for repository visual identity assets, generator scripts, and documentation.
+Verifies existence, dimensions, aspect ratios, file size bounds, animation frame integrity,
+and comprehensive cross-doc markdown reference validity.
"""
from pathlib import Path
@@ -20,6 +20,8 @@ def setUpClass(cls):
cls.assets_dir = cls.base_dir / ".github" / "assets"
cls.readme_path = cls.base_dir / "README.md"
cls.branding_path = cls.base_dir / "docs" / "BRANDING.md"
+ cls.arch_path = cls.base_dir / "docs" / "ARCHITECTURE.md"
+ cls.asset_catalog_path = cls.assets_dir / "README.md"
def test_assets_directory_exists(self):
"""The .github/assets directory must exist."""
@@ -56,7 +58,7 @@ def test_cryptographic_pipeline_asset(self):
self.assertEqual(im.size, (1376, 768))
def test_ephemeral_workflow_gif_asset(self):
- """Animated workflow GIF must exist, have multiple frames, and be under 2MB."""
+ """Animated workflow GIF must exist, have 58 frames, and be under 2MB."""
gif_path = self.assets_dir / "passli-ephemeral-workflow.gif"
self.assertTrue(gif_path.exists(), "passli-ephemeral-workflow.gif must exist")
self.assertLess(gif_path.stat().st_size, 2 * 1024 * 1024, "Workflow GIF must be under 2MB")
@@ -64,7 +66,18 @@ def test_ephemeral_workflow_gif_asset(self):
with Image.open(gif_path) as im:
self.assertEqual(im.format, "GIF")
self.assertTrue(getattr(im, "is_animated", False), "Workflow image must be an animated GIF")
- self.assertGreaterEqual(im.n_frames, 30, "Workflow GIF must contain at least 30 frames")
+ self.assertEqual(im.n_frames, 58, "Workflow GIF must contain exactly 58 distinct frames")
+ self.assertEqual(im.size, (1100, 580), "Workflow GIF dimensions must be 1100x580")
+
+ def test_vault_documents_asset(self):
+ """Vault documents preview must exist, be under 2MB, and have 16:9 aspect ratio."""
+ docs_path = self.assets_dir / "passli-vault-documents.png"
+ self.assertTrue(docs_path.exists(), "passli-vault-documents.png must exist")
+ self.assertLess(docs_path.stat().st_size, 2 * 1024 * 1024, "Docs preview must be under 2MB")
+
+ with Image.open(docs_path) as im:
+ self.assertEqual(im.format, "PNG")
+ self.assertEqual(im.size, (1376, 768))
def test_readme_references_assets(self):
"""README.md must reference the visual identity assets via valid relative paths."""
@@ -75,10 +88,21 @@ def test_readme_references_assets(self):
".github/assets/passli-repo-banner.png",
".github/assets/passli-ephemeral-workflow.gif",
".github/assets/passli-dashboard-preview.png",
+ ".github/assets/passli-vault-documents.png",
".github/assets/passli-cryptographic-pipeline.png",
]
for asset in required_assets:
self.assertIn(asset, content, f"README.md must contain relative link to {asset}")
+ target_file = self.base_dir / asset
+ self.assertTrue(target_file.exists(), f"Asset {asset} referenced in README.md must exist")
+
+ def test_architecture_doc_references_assets(self):
+ """docs/ARCHITECTURE.md must reference architecture and workflow assets."""
+ self.assertTrue(self.arch_path.exists(), "docs/ARCHITECTURE.md must exist")
+ content = self.arch_path.read_text(encoding="utf-8")
+
+ self.assertIn("../.github/assets/passli-cryptographic-pipeline.png", content)
+ self.assertIn("../.github/assets/passli-ephemeral-workflow.gif", content)
def test_branding_doc_exists_and_references_assets(self):
"""docs/BRANDING.md must exist and document brand tokens and assets."""
@@ -89,8 +113,26 @@ def test_branding_doc_exists_and_references_assets(self):
self.assertIn("#10B981", content)
self.assertIn("passli-repo-banner.png", content)
self.assertIn("passli-ephemeral-workflow.gif", content)
-
- def test_gif_generator_script_exists(self):
- """scripts/generate_ephemeral_flow_gif.py generator script must exist."""
- script_path = self.base_dir / "scripts" / "generate_ephemeral_flow_gif.py"
- self.assertTrue(script_path.exists(), "generate_ephemeral_flow_gif.py must exist")
+ self.assertIn("passli-dashboard-preview.png", content)
+ self.assertIn("passli-cryptographic-pipeline.png", content)
+
+ def test_asset_catalog_doc_references_all_assets(self):
+ """.github/assets/README.md must catalog all repository assets."""
+ self.assertTrue(self.asset_catalog_path.exists(), ".github/assets/README.md must exist")
+ content = self.asset_catalog_path.read_text(encoding="utf-8")
+
+ for asset_filename in [
+ "passli-repo-banner.png",
+ "passli-ephemeral-workflow.gif",
+ "passli-dashboard-preview.png",
+ "passli-cryptographic-pipeline.png",
+ ]:
+ self.assertIn(asset_filename, content)
+ self.assertTrue((self.assets_dir / asset_filename).exists())
+
+ def test_generator_scripts_exist(self):
+ """Scripts to generate GIF and dashboard preview must exist and be accessible."""
+ gif_script = self.base_dir / "scripts" / "generate_ephemeral_flow_gif.py"
+ dash_script = self.base_dir / "scripts" / "generate_dashboard_preview.py"
+ self.assertTrue(gif_script.exists(), "generate_ephemeral_flow_gif.py must exist")
+ self.assertTrue(dash_script.exists(), "generate_dashboard_preview.py must exist")