Fix lint: make scripts/demo/router.php PSR-12 compliant; tidy demo scripts

This commit is contained in:
2026-10-03 16:59:02 -04:00
parent 051ef6c915
commit 7edd438bac
4 changed files with 515 additions and 132 deletions
+1
View File
@@ -9,3 +9,4 @@ uploads/*
!uploads/.htaccess !uploads/.htaccess
# Composer dependencies (used only by the MCP endpoint; installed at deploy time) # Composer dependencies (used only by the MCP endpoint; installed at deploy time)
vendor/ vendor/
scripts/demo/__pycache__/
+151 -55
View File
@@ -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 PIL import Image
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
import pathlib import pathlib
OUT=str(pathlib.Path(__file__).resolve().parents[2]/'docs'/'img')
B='http://127.0.0.1:8300' OUT = str(pathlib.Path(__file__).resolve().parents[2] / "docs" / "img")
H={'Remote-User':'alex.rivera','Remote-Name':'Alex Rivera','Remote-Email':'alex@example.com','Remote-Groups':'admin,employee'} B = "http://127.0.0.1:8300"
P1='152847156'; CEPH='524938499' H = {
JS_TOP="document.activeElement&&document.activeElement.blur();document.documentElement.style.scrollBehavior='auto';window.scrollTo(0,0)" "Remote-User": "alex.rivera",
async def ctxmk(p,theme='dark',w=1440,h=900): "Remote-Name": "Alex Rivera",
b=await p.chromium.launch(); c=await b.new_context(viewport={'width':w,'height':h},extra_http_headers=H) "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){{}}") await c.add_init_script(f"try{{localStorage.setItem('lt_theme','{theme}')}}catch(e){{}}")
async def avatar(route): async def avatar(route):
import re 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') 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'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#050a10"/><text x="32" y="40" font-family="monospace" font-size="24" font-weight="700" fill="{col}" text-anchor="middle">{ini}</text></svg>' svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#050a10"/><text x="32" y="40" font-family="monospace" font-size="24" font-weight="700" fill="{col}" text-anchor="middle">{ini}</text></svg>'
await route.fulfill(status=200,content_type='image/svg+xml',body=svg) await route.fulfill(status=200, content_type="image/svg+xml", body=svg)
await c.route('**/api/user_avatar.php*',avatar)
await c.route("**/api/user_avatar.php*", avatar)
return b, c return b, c
async def go(pg, path, wait=3500): 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) 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): async def tab(pg, name):
t=pg.get_by_role('tab',name=name).first t = pg.get_by_role("tab", name=name).first
if await t.count()==0: t=pg.get_by_text(name,exact=False).first if await t.count() == 0:
await t.click(timeout=4000); await pg.wait_for_timeout(900) t = pg.get_by_text(name, exact=False).first
await t.click(timeout=4000)
await pg.wait_for_timeout(900)
async def shots(): async def shots():
async with async_playwright() as p: async with async_playwright() as p:
b,c=await ctxmk(p); pg=await c.new_page() b, c = await ctxmk(p)
await go(pg,'/',5000); await pg.screenshot(path=f'{OUT}/dashboard.png') pg = await c.new_page()
await go(pg, "/", 5000)
await pg.screenshot(path=f"{OUT}/dashboard.png")
# table crop # table crop
await pg.set_viewport_size({'width':1840,'height':900}); await pg.wait_for_timeout(600) await pg.set_viewport_size({"width": 1840, "height": 900})
await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})"); await pg.wait_for_timeout(500) await pg.wait_for_timeout(600)
await pg.screenshot(path=f'{OUT}/ticket-queue.png') await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})")
await pg.set_viewport_size({'width':1440,'height':900}); await pg.wait_for_timeout(600) await pg.wait_for_timeout(500)
try: await tab(pg,'Kanban'); await pg.screenshot(path=f'{OUT}/kanban.png'); print('kanban ok') await pg.screenshot(path=f"{OUT}/ticket-queue.png")
except Exception as e: print('kanban fail',str(e)[:80]) await pg.set_viewport_size({"width": 1440, "height": 900})
await go(pg,'/ticket/'+P1); await pg.screenshot(path=f'{OUT}/ticket-sla.png') await pg.wait_for_timeout(600)
await go(pg,'/ticket/'+CEPH) try:
for name,fn in (('Comments','ticket-comments'),('Dependencies','ticket-dependencies'),('Activity','ticket-activity')): await tab(pg, "Kanban")
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') await pg.screenshot(path=f"{OUT}/kanban.png")
except Exception as e: print(fn,'fail',str(e)[:80]) print("kanban ok")
await go(pg,'/ticket/create',3000); await pg.screenshot(path=f'{OUT}/new-ticket.png') except Exception as e:
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) print("kanban fail", str(e)[:80])
await pg.screenshot(path=f'{OUT}/command-palette.png', clip={'x':320,'y':60,'width':800,'height':460}) await go(pg, "/ticket/" + P1)
await pg.keyboard.press('Escape') await pg.screenshot(path=f"{OUT}/ticket-sla.png")
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, "/ticket/" + CEPH)
await go(pg,path,3500); await pg.screenshot(path=f'{OUT}/{name}.png'); print(name,'ok') 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() 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(): async def gif():
frames = [] frames = []
async with async_playwright() as p: async with async_playwright() as p:
b,c=await ctxmk(p,'dark',1280,720); pg=await c.new_page() b, c = await ctxmk(p, "dark", 1280, 720)
pg = await c.new_page()
async def snap(n=1, ms=0): async def snap(n=1, ms=0):
if ms: await pg.wait_for_timeout(ms) if ms:
im=Image.open(io.BytesIO(await pg.screenshot())).convert('RGB').resize((960,540),Image.LANCZOS) await pg.wait_for_timeout(ms)
for _ in range(n): frames.append(im) im = Image.open(io.BytesIO(await pg.screenshot())).convert("RGB").resize((960, 540), Image.LANCZOS)
await go(pg,'/',4500); await snap(3) for _ in range(n):
await pg.mouse.wheel(0,520); await snap(2,500) frames.append(im)
try: await tab(pg,'Kanban'); await snap(3,300)
except Exception: pass await go(pg, "/", 4500)
await go(pg,'/ticket/'+P1,3000); await snap(3) await snap(3)
await go(pg,'/ticket/'+CEPH,3000); await snap(2) await pg.mouse.wheel(0, 520)
for name in ('Comments','Dependencies','Activity'): await snap(2, 500)
try: await tab(pg,name); await snap(3,300) try:
except Exception: pass await tab(pg, "Kanban")
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 snap(3, 300)
await go(pg,'/admin/workflow',3000); await snap(3) 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() await b.close()
pal = [f.quantize(colors=96, method=Image.MEDIANCUT, dither=Image.NONE) for f in 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)) 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(): async def main():
if 'gif' not in sys.argv: await shots() if "gif" not in sys.argv:
if 'shots' not in sys.argv: await gif() await shots()
if "shots" not in sys.argv:
await gif()
asyncio.run(main()) asyncio.run(main())
+13 -3
View File
@@ -1,6 +1,16 @@
<?php <?php
$p = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$f = __DIR__ . '/tinker' . $p; /**
if ($p !== '/' && is_file($f) && !str_ends_with($f, '.php')) { return false; } * Router script for PHP's built-in web server, used only for the local screenshot demo.
* Serves real static files directly and sends everything else to the application's index.php.
*/
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$file = __DIR__ . '/tinker' . $path;
if ($path !== '/' && is_file($file) && !str_ends_with($file, '.php')) {
return false;
}
chdir(__DIR__ . '/tinker'); chdir(__DIR__ . '/tinker');
require __DIR__ . '/tinker/index.php'; require __DIR__ . '/tinker/index.php';
+330 -54
View File
@@ -1,68 +1,344 @@
import subprocess, hashlib, random, json # flake8: noqa: E501 (demo data and canned output contain long literal lines)
from datetime import datetime, timedelta import hashlib
random.seed(7) import json
import pathlib import pathlib
import random
from datetime import datetime, timedelta
random.seed(7)
D = str(pathlib.Path(__file__).resolve().parent) D = str(pathlib.Path(__file__).resolve().parent)
now = datetime.now().replace(microsecond=0) now = datetime.now().replace(microsecond=0)
def q(s): return "'"+str(s).replace("\\","\\\\").replace("'","\\'")+"'"
def ts(h): return q((now-timedelta(hours=h)).strftime('%Y-%m-%d %H:%M:%S'))
def q(s):
return "'" + str(s).replace("\\", "\\\\").replace("'", "\\'") + "'"
def ts(h):
return q((now - timedelta(hours=h)).strftime("%Y-%m-%d %H:%M:%S"))
sql = [] sql = []
users=[(1,'alex.rivera','Alex Rivera','alex@example.com','admin,employee',1), users = [
(2,'sam.chen','Sam Chen','sam@example.com','employee',0), (1, "alex.rivera", "Alex Rivera", "alex@example.com", "admin,employee", 1),
(3,'riley.brooks','Riley Brooks','riley@example.com','employee',0), (2, "sam.chen", "Sam Chen", "sam@example.com", "employee", 0),
(4,'jordan.kim','Jordan Kim','jordan@example.com','employee',0), (3, "riley.brooks", "Riley Brooks", "riley@example.com", "employee", 0),
(5,'hwmon-daemon','hwmonDaemon','','employee',0)] (4, "jordan.kim", "Jordan Kim", "jordan@example.com", "employee", 0),
for u in users: sql.append(f"INSERT INTO users(user_id,username,display_name,email,`groups`,is_admin,last_login) VALUES({u[0]},{q(u[1])},{q(u[2])},{q(u[3])},{q(u[4])},{u[5]},{ts(1)});") (5, "hwmon-daemon", "hwmonDaemon", "", "employee", 0),
tr=[('Open','In Progress',0,0),('Open','Pending',0,0),('Open','Closed',1,0),('Pending','Open',0,0),('Pending','In Progress',0,0),('Pending','Closed',1,0),('In Progress','Pending',0,0),('In Progress','Open',0,0),('In Progress','Closed',1,0),('Closed','Open',0,1)] ]
for a,b,c,d in tr: sql.append(f"INSERT INTO status_transitions(from_status,to_status,requires_comment,requires_admin) VALUES({q(a)},{q(b)},{c},{d});") for u in users:
sql.append(
f"INSERT INTO users(user_id,username,display_name,email,`groups`,is_admin,last_login) VALUES({u[0]},{q(u[1])},{q(u[2])},{q(u[3])},{q(u[4])},{u[5]},{ts(1)});"
)
tr = [
("Open", "In Progress", 0, 0),
("Open", "Pending", 0, 0),
("Open", "Closed", 1, 0),
("Pending", "Open", 0, 0),
("Pending", "In Progress", 0, 0),
("Pending", "Closed", 1, 0),
("In Progress", "Pending", 0, 0),
("In Progress", "Open", 0, 0),
("In Progress", "Closed", 1, 0),
("Closed", "Open", 0, 1),
]
for a, b, c, d in tr:
sql.append(
f"INSERT INTO status_transitions(from_status,to_status,requires_comment,requires_admin) VALUES({q(a)},{q(b)},{c},{d});"
)
# (title, cat, type, status, prio, assignee, creator, age_h, desc) # (title, cat, type, status, prio, assignee, creator, age_h, desc)
T = [ T = [
("[storage-01][auto][hardware] Drive SN-4F21A8 has SMART issues [single-node][production][problem]","Hardware","Problem","Open",2,2,5,3,"SMART reports 12 reallocated sectors and 1 pending sector on /dev/sdc.\n\n```\nReallocated_Sector_Ct 12\nCurrent_Pending_Sector 1\n```\nOSD.14 is backed by this drive. Plan a replacement in the next maintenance window."), (
("[node-02][auto][hardware] CPU temperature sustained above 85C [single-node][production][problem]","Hardware","Problem","Open",2,None,5,5,"15-minute average package temperature 87C (threshold 85C). Fans at 100%. Check airflow and heatsink seating."), "[storage-01][auto][hardware] Drive SN-4F21A8 has SMART issues [single-node][production][problem]",
("[proxmox-cluster][auto][software] Ceph HEALTH_WARN: 3 slow ops on osd.9 [cluster-wide][production][problem]","Software","Problem","In Progress",3,1,5,26,"`ceph health detail` reports slow BlueStore ops on osd.9. PGs remain active+clean. Triage steps in the Ceph runbook."), "Hardware",
("[node-03][auto][hardware] LXC storage pool above 85% usage [single-node][production][maintenance]","Hardware","Maintenance","Open",3,3,5,9,"Pool plpSSDPool is at 86%. Grow or move guests before it reaches 90%."), "Problem",
("[switch-02][auto][network] Link down on uplink port 14 [single-node][production][problem]","Network","Problem","Open",1,1,5,2,"Port 14 (10GbE uplink to node-03) went down 2 hours ago. Traffic failed over to the backup path. Cable or SFP+ suspected."), "Open",
("[proxmox-cluster][manual][software] Upgrade Proxmox VE 8.3 to 8.4 [cluster-wide][production][upgrade]","Software","Upgrade","Pending",3,2,1,70,"Rolling upgrade, one node at a time. Blocked until the Ceph warning on osd.9 is resolved."), 2,
("[monitor-01][manual][software] Renew TLS certificate for status page [single-node][production][maintenance]","Software","Maintenance","In Progress",4,4,1,30,"Certificate expires in 12 days. Renewal job failed on the DNS challenge."), 2,
("[proxmox-cluster][manual][security] Rotate API keys used by monitoring agents [cluster-wide][production][task]","Security","Task","Open",3,1,1,48,"Quarterly rotation. Keys: hwmonDaemon, gandalf, pulse workers."), 5,
("[node-01][auto][hardware] Drive SN-88C0D2 has SMART issues [single-node][production][problem]","Hardware","Problem","Closed",2,2,5,150,"Replaced during maintenance. OSD rebuilt, cluster healthy."), 3,
("[proxmox-cluster][manual][configuration] Add Prometheus scrape target for new exporter [cluster-wide][production][task]","General","Task","Closed",5,3,3,200,"Added and verified in Targets."), "SMART reports 12 reallocated sectors and 1 pending sector on /dev/sdc.\n\n```\nReallocated_Sector_Ct 12\nCurrent_Pending_Sector 1\n```\nOSD.14 is backed by this drive. Plan a replacement in the next maintenance window.",
("[node-02][manual][hardware] Replace failed NVMe in bay 2 [single-node][production][maintenance]","Hardware","Maintenance","Pending",2,2,1,52,"Replacement ordered. Awaiting delivery."), ),
("[storage-01][auto][hardware] Memory ECC correctable errors increasing [single-node][production][problem]","Hardware","Problem","Open",3,None,5,14,"EDAC reports 41 correctable errors on DIMM_B1 in 24 hours."), (
("[proxmox-cluster][manual][software] LXC 134 has no log retention configured [single-node][production][maintenance]","Software","Maintenance","Open",4,None,3,96,"Log volume grows without bound. Add retention and a compactor."), "[node-02][auto][hardware] CPU temperature sustained above 85C [single-node][production][problem]",
("[backup-01][manual][software] Verify weekly offsite backup restore [single-node][production][task]","General","Task","In Progress",4,4,1,40,"Restore one guest from the weekly backup and confirm it boots."), "Hardware",
("[switch-01][manual][network] Move camera VLAN to dedicated port group [single-node][testing][configuration]","Network","Task","Pending",5,3,3,120,"Needs a maintenance window."), "Problem",
("[node-03][auto][hardware] Fan 2 speed below threshold [single-node][production][problem]","Hardware","Problem","Closed",3,1,5,310,"Fan replaced."), "Open",
("[proxmox-cluster][manual][software] Document UPS shutdown order [cluster-wide][production][task]","General","Task","Closed",4,2,2,400,"Documented in the wiki."), 2,
("[node-02][auto][hardware] Drive SN-1B77E4 temperature above 55C [single-node][production][problem]","Hardware","Problem","Open",4,None,5,6,"Drive temperature 57C, threshold 55C."), None,
("[proxmox-cluster][manual][security] Review SSH access for decommissioned hosts [cluster-wide][production][task]","Security","Task","Pending",3,1,1,88,"Remove stale authorized_keys entries."), 5,
("[monitor-01][manual][software] Grafana alert rules need review [single-node][production][maintenance]","Software","Maintenance","Open",4,4,1,60,"Several rules have no contact point."), 5,
"15-minute average package temperature 87C (threshold 85C). Fans at 100%. Check airflow and heatsink seating.",
),
(
"[proxmox-cluster][auto][software] Ceph HEALTH_WARN: 3 slow ops on osd.9 [cluster-wide][production][problem]",
"Software",
"Problem",
"In Progress",
3,
1,
5,
26,
"`ceph health detail` reports slow BlueStore ops on osd.9. PGs remain active+clean. Triage steps in the Ceph runbook.",
),
(
"[node-03][auto][hardware] LXC storage pool above 85% usage [single-node][production][maintenance]",
"Hardware",
"Maintenance",
"Open",
3,
3,
5,
9,
"Pool plpSSDPool is at 86%. Grow or move guests before it reaches 90%.",
),
(
"[switch-02][auto][network] Link down on uplink port 14 [single-node][production][problem]",
"Network",
"Problem",
"Open",
1,
1,
5,
2,
"Port 14 (10GbE uplink to node-03) went down 2 hours ago. Traffic failed over to the backup path. Cable or SFP+ suspected.",
),
(
"[proxmox-cluster][manual][software] Upgrade Proxmox VE 8.3 to 8.4 [cluster-wide][production][upgrade]",
"Software",
"Upgrade",
"Pending",
3,
2,
1,
70,
"Rolling upgrade, one node at a time. Blocked until the Ceph warning on osd.9 is resolved.",
),
(
"[monitor-01][manual][software] Renew TLS certificate for status page [single-node][production][maintenance]",
"Software",
"Maintenance",
"In Progress",
4,
4,
1,
30,
"Certificate expires in 12 days. Renewal job failed on the DNS challenge.",
),
(
"[proxmox-cluster][manual][security] Rotate API keys used by monitoring agents [cluster-wide][production][task]",
"Security",
"Task",
"Open",
3,
1,
1,
48,
"Quarterly rotation. Keys: hwmonDaemon, gandalf, pulse workers.",
),
(
"[node-01][auto][hardware] Drive SN-88C0D2 has SMART issues [single-node][production][problem]",
"Hardware",
"Problem",
"Closed",
2,
2,
5,
150,
"Replaced during maintenance. OSD rebuilt, cluster healthy.",
),
(
"[proxmox-cluster][manual][configuration] Add Prometheus scrape target for new exporter [cluster-wide][production][task]",
"General",
"Task",
"Closed",
5,
3,
3,
200,
"Added and verified in Targets.",
),
(
"[node-02][manual][hardware] Replace failed NVMe in bay 2 [single-node][production][maintenance]",
"Hardware",
"Maintenance",
"Pending",
2,
2,
1,
52,
"Replacement ordered. Awaiting delivery.",
),
(
"[storage-01][auto][hardware] Memory ECC correctable errors increasing [single-node][production][problem]",
"Hardware",
"Problem",
"Open",
3,
None,
5,
14,
"EDAC reports 41 correctable errors on DIMM_B1 in 24 hours.",
),
(
"[proxmox-cluster][manual][software] LXC 134 has no log retention configured [single-node][production][maintenance]",
"Software",
"Maintenance",
"Open",
4,
None,
3,
96,
"Log volume grows without bound. Add retention and a compactor.",
),
(
"[backup-01][manual][software] Verify weekly offsite backup restore [single-node][production][task]",
"General",
"Task",
"In Progress",
4,
4,
1,
40,
"Restore one guest from the weekly backup and confirm it boots.",
),
(
"[switch-01][manual][network] Move camera VLAN to dedicated port group [single-node][testing][configuration]",
"Network",
"Task",
"Pending",
5,
3,
3,
120,
"Needs a maintenance window.",
),
(
"[node-03][auto][hardware] Fan 2 speed below threshold [single-node][production][problem]",
"Hardware",
"Problem",
"Closed",
3,
1,
5,
310,
"Fan replaced.",
),
(
"[proxmox-cluster][manual][software] Document UPS shutdown order [cluster-wide][production][task]",
"General",
"Task",
"Closed",
4,
2,
2,
400,
"Documented in the wiki.",
),
(
"[node-02][auto][hardware] Drive SN-1B77E4 temperature above 55C [single-node][production][problem]",
"Hardware",
"Problem",
"Open",
4,
None,
5,
6,
"Drive temperature 57C, threshold 55C.",
),
(
"[proxmox-cluster][manual][security] Review SSH access for decommissioned hosts [cluster-wide][production][task]",
"Security",
"Task",
"Pending",
3,
1,
1,
88,
"Remove stale authorized_keys entries.",
),
(
"[monitor-01][manual][software] Grafana alert rules need review [single-node][production][maintenance]",
"Software",
"Maintenance",
"Open",
4,
4,
1,
60,
"Several rules have no contact point.",
),
] ]
tids = [] tids = []
for i, t in enumerate(T): for i, t in enumerate(T):
tid=str(100000000+random.randint(1000000,899999999)); tids.append(tid) tid = str(100000000 + random.randint(1000000, 899999999))
tids.append(tid)
title, cat, typ, status, pr, asg, cr, age, desc = t title, cat, typ, status, pr, asg, cr, age, desc = t
closed=ts(max(age-5,1)) if status=='Closed' else 'NULL' closed = ts(max(age - 5, 1)) if status == "Closed" else "NULL"
sql.append(f"INSERT INTO tickets(ticket_id,title,category,type,status,description,created_at,updated_at,closed_at,priority,hash,created_by,updated_by,assigned_to) VALUES({q(tid)},{q(title)},{q(cat)},{q(typ)},{q(status)},{q(desc)},{ts(age)},{ts(max(age-3,1))},{closed},{pr},{q(hashlib.sha256(title.encode()).hexdigest())},{cr},{cr},{asg if asg else 'NULL'});") sql.append(
sql.append(f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({cr},'create','ticket',{q(tid)},{q(json.dumps({'title':title}))},{ts(age)});") f"INSERT INTO tickets(ticket_id,title,category,type,status,description,created_at,updated_at,closed_at,priority,hash,created_by,updated_by,assigned_to) VALUES({q(tid)},{q(title)},{q(cat)},{q(typ)},{q(status)},{q(desc)},{ts(age)},{ts(max(age-3,1))},{closed},{pr},{q(hashlib.sha256(title.encode()).hexdigest())},{cr},{cr},{asg if asg else 'NULL'});"
if asg: sql.append(f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES(1,'assign','ticket',{q(tid)},{q(json.dumps({'assigned_to':{'from':'','to':users[asg-1][2]}}))},{ts(max(age-1,1))});") )
if status!='Open': sql.append(f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({asg or 1},'status_change','ticket',{q(tid)},{q(json.dumps({'status':{'from':'Open','to':status}}))},{ts(max(age-2,1))});") sql.append(
C={0:[(2,"Confirmed on the node. Pulling the drive from the pool tonight and ordering a replacement.",2),(1,"Approved. Use the spare from the shelf, then reorder.",1)], f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({cr},'create','ticket',{q(tid)},{q(json.dumps({'title':title}))},{ts(age)});"
2:[(1,"Slow ops are isolated to osd.9. `ceph tell osd.9 dump_historic_slow_ops` shows long waits on the DB device.",20),(2,"Marked the OSD out for now. Recovery is progressing.",12)], )
if asg:
sql.append(
f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES(1,'assign','ticket',{q(tid)},{q(json.dumps({'assigned_to':{'from':'','to':users[asg-1][2]}}))},{ts(max(age-1,1))});"
)
if status != "Open":
sql.append(
f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({asg or 1},'status_change','ticket',{q(tid)},{q(json.dumps({'status':{'from':'Open','to':status}}))},{ts(max(age-2,1))});"
)
C = {
0: [
(2, "Confirmed on the node. Pulling the drive from the pool tonight and ordering a replacement.", 2),
(1, "Approved. Use the spare from the shelf, then reorder.", 1),
],
2: [
(
1,
"Slow ops are isolated to osd.9. `ceph tell osd.9 dump_historic_slow_ops` shows long waits on the DB device.",
20,
),
(2, "Marked the OSD out for now. Recovery is progressing.", 12),
],
4: [(1, "Failover worked as designed. Swapping the SFP+ module first.", 1)], 4: [(1, "Failover worked as designed. Swapping the SFP+ module first.", 1)],
5: [(2, "Waiting for the Ceph warning to clear before starting the first node.", 30)], 5: [(2, "Waiting for the Ceph warning to clear before starting the first node.", 30)],
6:[(4,"DNS challenge fails because the API token expired. Generated a new one.",8)]} 6: [(4, "DNS challenge fails because the API token expired. Generated a new one.", 8)],
}
for idx, cs in C.items(): for idx, cs in C.items():
for uid, txt, age in cs: for uid, txt, age in cs:
sql.append(f"INSERT INTO ticket_comments(ticket_id,user_name,comment_text,created_at,markdown_enabled,user_id) VALUES({q(tids[idx])},{q(users[uid-1][2])},{q(txt)},{ts(age)},1,{uid});") sql.append(
sql.append(f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({uid},'comment','comment','0',{q(json.dumps({'ticket_id':tids[idx]}))},{ts(age)});") f"INSERT INTO ticket_comments(ticket_id,user_name,comment_text,created_at,markdown_enabled,user_id) VALUES({q(tids[idx])},{q(users[uid-1][2])},{q(txt)},{ts(age)},1,{uid});"
sql.append(f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[5])},{q(tids[2])},'blocked_by',1);") )
sql.append(f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[0])},{q(tids[8])},'relates_to',1);") sql.append(
sql.append(f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[4])},{q(tids[14])},'relates_to',1);") f"INSERT INTO audit_log(user_id,action_type,entity_type,entity_id,details,created_at) VALUES({uid},'comment','comment','0',{q(json.dumps({'ticket_id':tids[idx]}))},{ts(age)});"
for tid in (tids[0],tids[2],tids[4]): sql.append(f"INSERT INTO ticket_watchers(ticket_id,user_id) VALUES({q(tid)},1);") )
for n,p,s in (('hwmonDaemon (prod)','hwm_9f3a','read_write'),('gandalf','gnd_71c2','read_write'),('readonly-dashboard','ro_55de','read')): sql.append(
sql.append(f"INSERT INTO api_keys(key_name,key_hash,key_prefix,scope,created_by,last_used) VALUES({q(n)},{q(hashlib.sha256(p.encode()).hexdigest())},{q(p)},{q(s)},1,{ts(2)});") f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[5])},{q(tids[2])},'blocked_by',1);"
sql.append(f"INSERT INTO recurring_tickets(title_template,description_template,category,type,priority,schedule_type,schedule_day,next_run_at,created_by) VALUES('[proxmox-cluster][auto][software] Monthly patch review [cluster-wide][production][maintenance]','Review pending updates and schedule a window.','Software','Maintenance',4,'monthly',1,{ts(-200)},1);") )
sql.append(f"INSERT INTO recurring_tickets(title_template,description_template,category,type,priority,schedule_type,schedule_day,next_run_at,created_by) VALUES('[backup-01][auto][software] Weekly restore test [single-node][production][task]','Restore one guest and verify.','General','Task',4,'weekly',1,{ts(-60)},1);") sql.append(
open(D+'/seed.sql','w').write('\n'.join(sql)) f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[0])},{q(tids[8])},'relates_to',1);"
print(len(T),'tickets; ids sample',tids[:3]) )
sql.append(
f"INSERT INTO ticket_dependencies(ticket_id,depends_on_id,dependency_type,created_by) VALUES({q(tids[4])},{q(tids[14])},'relates_to',1);"
)
for tid in (tids[0], tids[2], tids[4]):
sql.append(f"INSERT INTO ticket_watchers(ticket_id,user_id) VALUES({q(tid)},1);")
for n, p, s in (
("hwmonDaemon (prod)", "hwm_9f3a", "read_write"),
("gandalf", "gnd_71c2", "read_write"),
("readonly-dashboard", "ro_55de", "read"),
):
sql.append(
f"INSERT INTO api_keys(key_name,key_hash,key_prefix,scope,created_by,last_used) VALUES({q(n)},{q(hashlib.sha256(p.encode()).hexdigest())},{q(p)},{q(s)},1,{ts(2)});"
)
sql.append(
f"INSERT INTO recurring_tickets(title_template,description_template,category,type,priority,schedule_type,schedule_day,next_run_at,created_by) VALUES('[proxmox-cluster][auto][software] Monthly patch review [cluster-wide][production][maintenance]','Review pending updates and schedule a window.','Software','Maintenance',4,'monthly',1,{ts(-200)},1);"
)
sql.append(
f"INSERT INTO recurring_tickets(title_template,description_template,category,type,priority,schedule_type,schedule_day,next_run_at,created_by) VALUES('[backup-01][auto][software] Weekly restore test [single-node][production][task]','Restore one guest and verify.','General','Task',4,'weekly',1,{ts(-60)},1);"
)
open(D + "/seed.sql", "w").write("\n".join(sql))
print(len(T), "tickets; ids sample", tids[:3])