diff --git a/.gitignore b/.gitignore
index ee4c9de..215e891 100755
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ uploads/*
!uploads/.htaccess
# Composer dependencies (used only by the MCP endpoint; installed at deploy time)
vendor/
+scripts/demo/__pycache__/
diff --git a/scripts/demo/capture.py b/scripts/demo/capture.py
index 30d2ece..789cce7 100644
--- a/scripts/demo/capture.py
+++ b/scripts/demo/capture.py
@@ -1,78 +1,174 @@
-import asyncio, io, sys
+# flake8: noqa: E501 (demo data and canned output contain long literal lines)
+import asyncio
+import io
+import sys
from PIL import Image
from playwright.async_api import async_playwright
import pathlib
-OUT=str(pathlib.Path(__file__).resolve().parents[2]/'docs'/'img')
-B='http://127.0.0.1:8300'
-H={'Remote-User':'alex.rivera','Remote-Name':'Alex Rivera','Remote-Email':'alex@example.com','Remote-Groups':'admin,employee'}
-P1='152847156'; CEPH='524938499'
-JS_TOP="document.activeElement&&document.activeElement.blur();document.documentElement.style.scrollBehavior='auto';window.scrollTo(0,0)"
-async def ctxmk(p,theme='dark',w=1440,h=900):
- b=await p.chromium.launch(); c=await b.new_context(viewport={'width':w,'height':h},extra_http_headers=H)
+
+OUT = str(pathlib.Path(__file__).resolve().parents[2] / "docs" / "img")
+B = "http://127.0.0.1:8300"
+H = {
+ "Remote-User": "alex.rivera",
+ "Remote-Name": "Alex Rivera",
+ "Remote-Email": "alex@example.com",
+ "Remote-Groups": "admin,employee",
+}
+P1 = "152847156"
+CEPH = "524938499"
+JS_TOP = (
+ "document.activeElement&&document.activeElement.blur();"
+ "document.documentElement.style.scrollBehavior='auto';window.scrollTo(0,0)"
+)
+
+
+async def ctxmk(p, theme="dark", w=1440, h=900):
+ b = await p.chromium.launch()
+ c = await b.new_context(viewport={"width": w, "height": h}, extra_http_headers=H)
await c.add_init_script(f"try{{localStorage.setItem('lt_theme','{theme}')}}catch(e){{}}")
+
async def avatar(route):
import re
- uid=int((re.search(r'user_id=(\d+)',route.request.url) or [0,0])[1] or 0)
- ini={1:'AR',2:'SC',3:'RB',4:'JK',5:'HW'}.get(uid,'??'); col={1:'#ff6b00',2:'#00d4ff',3:'#b44aff',4:'#00ff88',5:'#ffc800'}.get(uid,'#888')
- svg=f''
- await route.fulfill(status=200,content_type='image/svg+xml',body=svg)
- await c.route('**/api/user_avatar.php*',avatar)
- return b,c
-async def go(pg,path,wait=3500):
- await pg.goto(B+path); await pg.wait_for_timeout(wait); await pg.evaluate(JS_TOP); await pg.wait_for_timeout(300)
-async def tab(pg,name):
- t=pg.get_by_role('tab',name=name).first
- if await t.count()==0: t=pg.get_by_text(name,exact=False).first
- await t.click(timeout=4000); await pg.wait_for_timeout(900)
+
+ uid = int((re.search(r"user_id=(\d+)", route.request.url) or [0, 0])[1] or 0)
+ ini = {1: "AR", 2: "SC", 3: "RB", 4: "JK", 5: "HW"}.get(uid, "??")
+ col = {1: "#ff6b00", 2: "#00d4ff", 3: "#b44aff", 4: "#00ff88", 5: "#ffc800"}.get(uid, "#888")
+ svg = f''
+ await route.fulfill(status=200, content_type="image/svg+xml", body=svg)
+
+ await c.route("**/api/user_avatar.php*", avatar)
+ return b, c
+
+
+async def go(pg, path, wait=3500):
+ await pg.goto(B + path)
+ await pg.wait_for_timeout(wait)
+ await pg.evaluate(JS_TOP)
+ await pg.wait_for_timeout(300)
+
+
+async def tab(pg, name):
+ t = pg.get_by_role("tab", name=name).first
+ if await t.count() == 0:
+ t = pg.get_by_text(name, exact=False).first
+ await t.click(timeout=4000)
+ await pg.wait_for_timeout(900)
+
+
async def shots():
async with async_playwright() as p:
- b,c=await ctxmk(p); pg=await c.new_page()
- await go(pg,'/',5000); await pg.screenshot(path=f'{OUT}/dashboard.png')
+ b, c = await ctxmk(p)
+ pg = await c.new_page()
+ await go(pg, "/", 5000)
+ await pg.screenshot(path=f"{OUT}/dashboard.png")
# table crop
- await pg.set_viewport_size({'width':1840,'height':900}); await pg.wait_for_timeout(600)
- await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})"); await pg.wait_for_timeout(500)
- await pg.screenshot(path=f'{OUT}/ticket-queue.png')
- await pg.set_viewport_size({'width':1440,'height':900}); await pg.wait_for_timeout(600)
- try: await tab(pg,'Kanban'); await pg.screenshot(path=f'{OUT}/kanban.png'); print('kanban ok')
- except Exception as e: print('kanban fail',str(e)[:80])
- await go(pg,'/ticket/'+P1); await pg.screenshot(path=f'{OUT}/ticket-sla.png')
- await go(pg,'/ticket/'+CEPH)
- for name,fn in (('Comments','ticket-comments'),('Dependencies','ticket-dependencies'),('Activity','ticket-activity')):
- try: await tab(pg,name); await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})"); await pg.wait_for_timeout(500); await pg.screenshot(path=f'{OUT}/{fn}.png'); print(fn,'ok')
- except Exception as e: print(fn,'fail',str(e)[:80])
- await go(pg,'/ticket/create',3000); await pg.screenshot(path=f'{OUT}/new-ticket.png')
- await go(pg,'/'); await pg.keyboard.press('Control+k'); await pg.wait_for_timeout(500); await pg.keyboard.type('ticket',delay=70); await pg.wait_for_timeout(900)
- await pg.screenshot(path=f'{OUT}/command-palette.png', clip={'x':320,'y':60,'width':800,'height':460})
- await pg.keyboard.press('Escape')
- for path,name in (('/admin/workflow','admin-workflow'),('/admin/api-keys','admin-api-keys'),('/admin/audit-log','admin-audit-log'),('/admin/recurring-tickets','admin-recurring')):
- await go(pg,path,3500); await pg.screenshot(path=f'{OUT}/{name}.png'); print(name,'ok')
+ await pg.set_viewport_size({"width": 1840, "height": 900})
+ await pg.wait_for_timeout(600)
+ await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})")
+ await pg.wait_for_timeout(500)
+ await pg.screenshot(path=f"{OUT}/ticket-queue.png")
+ await pg.set_viewport_size({"width": 1440, "height": 900})
+ await pg.wait_for_timeout(600)
+ try:
+ await tab(pg, "Kanban")
+ await pg.screenshot(path=f"{OUT}/kanban.png")
+ print("kanban ok")
+ except Exception as e:
+ print("kanban fail", str(e)[:80])
+ await go(pg, "/ticket/" + P1)
+ await pg.screenshot(path=f"{OUT}/ticket-sla.png")
+ await go(pg, "/ticket/" + CEPH)
+ for name, fn in (
+ ("Comments", "ticket-comments"),
+ ("Dependencies", "ticket-dependencies"),
+ ("Activity", "ticket-activity"),
+ ):
+ try:
+ await tab(pg, name)
+ await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})")
+ await pg.wait_for_timeout(500)
+ await pg.screenshot(path=f"{OUT}/{fn}.png")
+ print(fn, "ok")
+ except Exception as e:
+ print(fn, "fail", str(e)[:80])
+ await go(pg, "/ticket/create", 3000)
+ await pg.screenshot(path=f"{OUT}/new-ticket.png")
+ await go(pg, "/")
+ await pg.keyboard.press("Control+k")
+ await pg.wait_for_timeout(500)
+ await pg.keyboard.type("ticket", delay=70)
+ await pg.wait_for_timeout(900)
+ await pg.screenshot(path=f"{OUT}/command-palette.png", clip={"x": 320, "y": 60, "width": 800, "height": 460})
+ await pg.keyboard.press("Escape")
+ for path, name in (
+ ("/admin/workflow", "admin-workflow"),
+ ("/admin/api-keys", "admin-api-keys"),
+ ("/admin/audit-log", "admin-audit-log"),
+ ("/admin/recurring-tickets", "admin-recurring"),
+ ):
+ await go(pg, path, 3500)
+ await pg.screenshot(path=f"{OUT}/{name}.png")
+ print(name, "ok")
await b.close()
- b,c=await ctxmk(p,'light'); pg=await c.new_page(); await go(pg,'/',5000); await pg.screenshot(path=f'{OUT}/dashboard-light.png'); await b.close()
+ b, c = await ctxmk(p, "light")
+ pg = await c.new_page()
+ await go(pg, "/", 5000)
+ await pg.screenshot(path=f"{OUT}/dashboard-light.png")
+ await b.close()
+
+
async def gif():
- frames=[]
+ frames = []
async with async_playwright() as p:
- b,c=await ctxmk(p,'dark',1280,720); pg=await c.new_page()
- async def snap(n=1,ms=0):
- if ms: await pg.wait_for_timeout(ms)
- im=Image.open(io.BytesIO(await pg.screenshot())).convert('RGB').resize((960,540),Image.LANCZOS)
- for _ in range(n): frames.append(im)
- await go(pg,'/',4500); await snap(3)
- await pg.mouse.wheel(0,520); await snap(2,500)
- try: await tab(pg,'Kanban'); await snap(3,300)
- except Exception: pass
- await go(pg,'/ticket/'+P1,3000); await snap(3)
- await go(pg,'/ticket/'+CEPH,3000); await snap(2)
- for name in ('Comments','Dependencies','Activity'):
- try: await tab(pg,name); await snap(3,300)
- except Exception: pass
- await go(pg,'/',3000); await pg.keyboard.press('Control+k'); await snap(2,500)
- for ch in 'tick': await pg.keyboard.type(ch); await snap(1,200)
- await snap(3,300)
- await go(pg,'/admin/workflow',3000); await snap(3)
+ b, c = await ctxmk(p, "dark", 1280, 720)
+ pg = await c.new_page()
+
+ async def snap(n=1, ms=0):
+ if ms:
+ await pg.wait_for_timeout(ms)
+ im = Image.open(io.BytesIO(await pg.screenshot())).convert("RGB").resize((960, 540), Image.LANCZOS)
+ for _ in range(n):
+ frames.append(im)
+
+ await go(pg, "/", 4500)
+ await snap(3)
+ await pg.mouse.wheel(0, 520)
+ await snap(2, 500)
+ try:
+ await tab(pg, "Kanban")
+ await snap(3, 300)
+ except Exception:
+ pass
+ await go(pg, "/ticket/" + P1, 3000)
+ await snap(3)
+ await go(pg, "/ticket/" + CEPH, 3000)
+ await snap(2)
+ for name in ("Comments", "Dependencies", "Activity"):
+ try:
+ await tab(pg, name)
+ await snap(3, 300)
+ except Exception:
+ pass
+ await go(pg, "/", 3000)
+ await pg.keyboard.press("Control+k")
+ await snap(2, 500)
+ for ch in "tick":
+ await pg.keyboard.type(ch)
+ await snap(1, 200)
+ await snap(3, 300)
+ await go(pg, "/admin/workflow", 3000)
+ await snap(3)
await b.close()
- pal=[f.quantize(colors=96,method=Image.MEDIANCUT,dither=Image.NONE) for f in frames]
- pal[0].save(f'{OUT}/demo.gif',save_all=True,append_images=pal[1:],duration=1100,loop=0,optimize=True); print('gif',len(frames))
+ pal = [f.quantize(colors=96, method=Image.MEDIANCUT, dither=Image.NONE) for f in frames]
+ pal[0].save(f"{OUT}/demo.gif", save_all=True, append_images=pal[1:], duration=1100, loop=0, optimize=True)
+ print("gif", len(frames))
+
+
async def main():
- if 'gif' not in sys.argv: await shots()
- if 'shots' not in sys.argv: await gif()
+ if "gif" not in sys.argv:
+ await shots()
+ if "shots" not in sys.argv:
+ await gif()
+
+
asyncio.run(main())
diff --git a/scripts/demo/router.php b/scripts/demo/router.php
index e7d05d7..ca6f9e2 100644
--- a/scripts/demo/router.php
+++ b/scripts/demo/router.php
@@ -1,6 +1,16 @@