sync: auto-sync from DESKTOP-0O8A1RL at 2026-05-23 11:05:00

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 10:56:40 -07:00
parent fd2bab3614
commit 288ff122ca
6 changed files with 1294 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import sys
import base64
path = "/home/guru/gururmm/dashboard/src/pages/Alerts.tsx"
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# Component encoded as base64 to avoid any quoting issues in this script
# Decoded content is the WatchdogAlertsSection TSX component
COMPONENT_B64 = (
"Cgov"
"LyBXYXRjaGRvZ0FsZXJ0c1NlY3Rpb24KCnR5cGUgV2RvZ1N0YXR1cyA9ICJhY3RpdmUiIHwg"
"ImFja25vd2xlZGdlZCIgfCAicmVzb2x2ZWQiOwoKZnVuY3Rpb24gd2RvZ1N0YXR1cyhhbGVy"
"dDogV2F0Y2hkb2dBbGVydCk6IFdkb2dTdGF0dXMgewogIGlmIChhbGVydC5yZXNvbHZlZF9h"
"dCkgcmV0dXJuICJyZXNvbHZlZCI7CiAgaWYgKGFsZXJ0LmFja25vd2xlZGdlZF9hdCkgcmV0"
"dXJuICJhY2tub3dsZWRnZWQiOwogIHJldHVybiAiYWN0aXZlIjsKfQoKZnVuY3Rpb24gV2F0"
"Y2hkb2dBbGVydHNTZWN0aW9uKCkgewogIGNvbnN0IHF1ZXJ5Q2xpZW50ID0gdXNlUXVlcnlD"
"bGllbnQoKTsKICBjb25zdCB7IHRvYXN0IH0gPSB1c2VUb2FzdCgpOwogIGNvbnN0IFtzaG93"
"QWxsLCBzZXRTaG93QWxsXSA9IHVzZVN0YXRlKGZhbHNlKTsKICBjb25zdCBbZXhwYW5kZWRM"
"b2dJZCwgc2V0RXhwYW5kZWRMb2dJZF0gPSB1c2VTdGF0ZTxzdHJpbmcgfCBudWxsPihudWxs"
"KTsKCiAgY29uc3QgeyBkYXRhOiBhbGxBbGVydHMgPSBbXSwgaXNMb2FkaW5nIH0gPSB1c2VR"
"dWVyeSh7CiAgICBxdWVyeUtleTogWyJ3YXRjaGRvZy1hbGVydHMiXSwKICAgIHF1ZXJ5Rm46"
"ICgpID0+IHdhdGNoZG9nQWxlcnRzQXBpLmxpc3QoKS50aGVuKChyKSA9PiByLmRhdGEpLAog"
"ICAgcmVmZXRjaEludGVydmFsOiAzMDAwMCwKICB9KTsKCiAgY29uc3QgYWxlcnRzID0gc2hv"
"d0FsbCA/IGFsbEFsZXJ0cyA6IGFsbEFsZXJ0cy5maWx0ZXIoKGEpID0+ICFhLnJlc29sdmVk"
"X2F0KTsKCiAgY29uc3QgYWNrbm93bGVkZ2VNdXRhdGlvbiA9IHVzZU11dGF0aW9uKHsKICAg"
"IG11dGF0aW9uRm46IChpZDogc3RyaW5nKSA9PiB3YXRjaGRvZ0FsZXJ0c0FwaS5hY2tub3ds"
"ZWRnZShpZCksCiAgICBvblN1Y2Nlc3M6ICgpID0+IHsKICAgICAgcXVlcnlDbGllbnQuaW52"
"YWxpZGF0ZVF1ZXJpZXMoeyBxdWVyeUtleTogWyJ3YXRjaGRvZy1hbGVydHMiXSB9KTsKICAg"
"ICAgdG9hc3QoeyB0eXBlOiAic3VjY2VzcyIsIHRpdGxlOiAiQWxlcnQgYWNrbm93bGVkZ2Vk"
"IiB9KTsKICAgIH0sCiAgICBvbkVycm9yOiAoZXJyOiBFcnJvcikgPT4KICAgICAgdG9hc3Qo"
"eyB0eXBlOiAiZXJyb3IiLCB0aXRsZTogIkNvdWxkIG5vdCBhY2tub3dsZWRnZSIsIG1lc3Nh"
"Z2U6IGVyci5tZXNzYWdlIH0pLAogIH0pOwoKICBjb25zdCByZXNvbHZlTXV0YXRpb24gPSB1"
"c2VNdXRhdGlvbih7CiAgICBtdXRhdGlvbkZuOiAoaWQ6IHN0cmluZykgPT4gd2F0Y2hkb2dB"
"bGVydHNBcGkucmVzb2x2ZShpZCksCiAgICBvblN1Y2Nlc3M6ICgpID0+IHsKICAgICAgcXVl"
"cnlDbGllbnQuaW52YWxpZGF0ZVF1ZXJpZXMoeyBxdWVyeUtleTogWyJ3YXRjaGRvZy1hbGVy"
"dHMiXSB9KTsKICAgICAgdG9hc3QoeyB0eXBlOiAic3VjY2VzcyIsIHRpdGxlOiAiQWxlcnQg"
"cmVzb2x2ZWQiIH0pOwogICAgfSwKICAgIG9uRXJyb3I6IChlcnI6IEVycm9yKSA9PgogICAg"
"ICB0b2FzdCh7IHR5cGU6ICJlcnJvciIsIHRpdGxlOiAiQ291bGQgbm90IHJlc29sdmUiLCBt"
"ZXNzYWdlOiBlcnIubWVzc2FnZSB9KSwKICB9KTsKCiAgY29uc3QgZGVsZXRlTXV0YXRpb24g"
"PSB1c2VNdXRhdGlvbih7CiAgICBtdXRhdGlvbkZuOiAoaWQ6IHN0cmluZykgPT4gd2F0Y2hk"
"b2dBbGVydHNBcGkuZGVsZXRlKGlkKSwKICAgIG9uU3VjY2VzczogKCkgPT4gewogICAgICBx"
"dWVyeUNsaWVudC5pbnZhbGlkYXRlUXVlcmllcyh7IHF1ZXJ5S2V5OiBbIndhdGNoZG9nLWFs"
"ZXJ0cyJdIH0pOwogICAgICB0b2FzdCh7IHR5cGU6ICJzdWNjZXNzIiwgdGl0bGU6ICJBbGVy"
"dCBkZWxldGVkIiB9KTsKICAgIH0sCiAgICBvbkVycm9yOiAoZXJyOiBFcnJvcikgPT4KICAg"
"ICAgdG9hc3QoeyB0eXBlOiAiZXJyb3IiLCB0aXRsZTogIkNvdWxkIG5vdCBkZWxldGUiLCBt"
"ZXNzYWdlOiBlcnIubWVzc2FnZSB9KSwKICB9KTsKCiAgY29uc3QgaXNNdXRhdGluZyA9CiAg"
"ICBhY2tub3dsZWRnZU11dGF0aW9uLmlzUGVuZGluZyB8fAogICAgcmVzb2x2ZU11dGF0aW9u"
"LmlzUGVuZGluZyB8fAogICAgZGVsZXRlTXV0YXRpb24uaXNQZW5kaW5nOwoKICBjb25zdCBh"
"Y3RpdmVDb3VudCA9IGFsbEFsZXJ0cy5maWx0ZXIoKGEpID0+IHdkb2dTdGF0dXMoYSkgPT09"
"ICJhY3RpdmUiKS5sZW5ndGg7CgogIHJldHVybiAoCiAgICA8Q2FyZD4KICAgICAgPENhcmRI"
"ZWFkZXI+CiAgICAgICAgPGRpdiBjbGFzc05hbWU9ImZsZXggaXRlbXMtY2VudGVyIGp1c3Rp"
"ZnktYmV0d2VlbiBnYXAtMyI+CiAgICAgICAgICA8Q2FyZFRpdGxlIGNsYXNzTmFtZT0iZmxl"
"eCBpdGVtcy1jZW50ZXIgZ2FwLTIiPgogICAgICAgICAgICA8QWxlcnRUcmlhbmdsZSBjbGFz"
"c05hbWU9Img0IHctNCB0ZXh0LWFtYmVyLTUwMCIgLz4KICAgICAgICAgICAgV2F0Y2hkb2cg"
"QWxlcnRzCiAgICAgICAgICAgIHthY3RpdmVDb3VudCA+IDAgJiYgKAogICAgICAgICAgICAg"
"IDxzcGFuIGNsYXNzTmFtZT0ibWwtMSByb3VuZGVkLWZ1bGwgYmctYW1iZXItNTAwLzE1IHB4"
"LTIgcHktMC41IHRleHQteHMgZm9udC1tZWRpdW0gdGV4dC1hbWJlci02MDAgZGFyazp0ZXh0"
"LWFtYmVyLTQwMCI+CiAgICAgICAgICAgICAgICB7YWN0aXZlQ291bnR9IGFjdGl2ZQogICAg"
"ICAgICAgICAgIDwvc3Bhbj4KICAgICAgICAgICAgKX0KICAgICAgICAgIDwvQ2FyZFRpdGxlPgog"
"ICAgICAgICAgPGJ1dHRvbgogICAgICAgICAgICBjbGFzc05hbWU9InRleHQteHMgdGV4dC1baHNs"
"KHZhcigtLW11dGVkLWZvcmVncm91bmQpKV0gaG92ZXI6dGV4dC1baHNsKHZhcigtLWZvcmVn"
"cm91bmQpKV0gdW5kZXJsaW5lLW9mZnNldC0yIGhvdmVyOnVuZGVybGluZSIKICAgICAgICAg"
"ICAgb25DbGljaz17KCkgPT4gc2V0U2hvd0FsbCgodikgPT4gIXYpfQogICAgICAgICAgPgog"
"ICAgICAgICAgICB7c2hvd0FsbCA/ICJBY3RpdmUgb25seSIgOiAiU2hvdyBhbGwifQogICAg"
"ICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvQ2FyZEhlYWRlcj4KICAg"
"ICAgPENhcmRDb250ZW50PgogICAgICAgIHtpc0xvYWRpbmcgJiYgKAogICAgICAgICAgPHAg"
"Y2xhc3NOYW1lPSJweS02IHRleHQtY2VudGVyIHRleHQtc20gdGV4dC1baHNsKHZhcigtLW11"
"dGVkLWZvcmVncm91bmQpKV0iPkxvYWRpbmcuLi48L3A+CiAgICAgICAgKX0KICAgICAgICB7"
"IWlzTG9hZGluZyAmJiBhbGVydHMubGVuZ3RoID09PSAwICYmICgKICAgICAgICAgIDxwIGNs"
"YXNzTmFtZT0icHktNiB0ZXh0LWNlbnRlciB0ZXh0LXNtIHRleHQtW2hzbCh2YXIoLS1tdXRl"
"ZC1mb3JlZ3JvdW5kKSldIj4KICAgICAgICAgICAge3Nob3dBbGwgPyAiTm8gd2F0Y2hkb2cg"
"YWxlcnRzIHJlY29yZGVkLiIgOiAiTm8gYWN0aXZlIHdhdGNoZG9nIGFsZXJ0cy4ifQogICAg"
"ICAgICAgPC9wPgogICAgICAgICl9CiAgICAgICAgPGRpdiBjbGFzc05hbWU9InNwYWNlLXktMyI+"
"CiAgICAgICAgICB7YWxlcnRzLm1hcCgoYWxlcnQpID0+IHsKICAgICAgICAgICAgY29uc3Qg"
"c3RhdHVzID0gd2RvZ1N0YXR1cyhhbGVydCk7CiAgICAgICAgICAgIGNvbnN0IGxvZ0V4cGFu"
"ZGVkID0gZXhwYW5kZWRMb2dJZCA9PT0gYWxlcnQuaWQ7CiAgICAgICAgICAgIGNvbnN0IHN0"
"YXR1c0NvbG9yID0KICAgICAgICAgICAgICBzdGF0dXMgPT09ICJhY3RpdmUiCiAgICAgICAg"
"ICAgICAgICA/ICJ0ZXh0LXJlZC02MDAgZGFyazp0ZXh0LXJlZC00MDAiCiAgICAgICAgICAg"
"ICAgICAgIDogc3RhdHVzID09PSAiYWNrbm93bGVkZ2VkIgogICAgICAgICAgICAgICAgPyAi"
"dGV4dC1hbWJlci02MDAgZGFyazp0ZXh0LWFtYmVyLTQwMCIKICAgICAgICAgICAgICAgIDog"
"InRleHQtW2hzbCh2YXIoLS1tdXRlZC1mb3JlZ3JvdW5kKSldIjsKCiAgICAgICAgICAgIHJl"
"dHVybiAoCiAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAga2V5PXthbGVydC5pZH0K"
"ICAgICAgICAgICAgICAgIGNsYXNzTmFtZT0icm91bmRlZC1sZyBib3JkZXIgYm9yZGVyLVto"
"c2wodmFyKC0tYm9yZGVyKSldIHA0IHNwYWNlLXktMiIKICAgICAgICAgICAgICA+CiAgICAg"
"ICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0iZmxleCBpdGVtcy1zdGFydCBqdXN0aWZ5LWJl"
"dHdlZW4gZ2FwLTQiPgogICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ic3BhY2Ut"
"eS0wLjUiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1z"
"LWNlbnRlciBnYXAtMiI+CiAgICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9"
"e2B0ZXh0LXhzIGZvbnQtc2VtaWJvbGQgdXBwZXJjYXNlIHRyYWNraW5nLXdpZGVyICR7c3Rh"
"dHVzQ29sb3J9YH0+CiAgICAgICAgICAgICAgICAgICAgICAgIHtzdGF0dXN9CiAgICAgICAg"
"ICAgICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFz"
"c05hbWU9InRleHQteHMgdGV4dC1baHNsKHZhcigtLW11dGVkLWZvcmVncm91bmQpKV0iPgog"
"ICAgICAgICAgICAgICAgICAgICAgICAgwrcge2FsZXJ0LnJlc3RhcnRfYXR0ZW1wdHN9IHJl"
"c3RhcnQgYXR0ZW1wdAogICAgICAgICAgICAgICAgICAgICAgICB7YWxlcnQucmVzdGFydF9h"
"dHRlbXB0cyAhPT0gMSA/ICJzIiA6ICIifQogICAgICAgICAgICAgICAgICAgICAgPC9zcGFu"
"PgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDxwIGNs"
"YXNzTmFtZT0idGV4dC14cyB0ZXh0LVtoc2wodmFyKC0tbXV0ZWQtZm9yZWdyb3VuZCkpXSI+"
"CiAgICAgICAgICAgICAgICAgICAgICBBZ2VudCBJRDp7IiAifQogICAgICAgICAgICAgICAg"
"ICAgICAgPHNwYW4gY2xhc3NOYW1lPSJmb250LW1vbm8iIHRpdGxlPXthbGVydC5hZ2VudF9p"
"ZH0+CiAgICAgICAgICAgICAgICAgICAgICAgIHthbGVydC5hZ2VudF9pZC5zbGljZSgwLCA4"
"KX0mIzgyMzA7CiAgICAgICAgICAgICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICAgICAg"
"ICAgICAgICB7IiAifcK3eyIgIn0KICAgICAgICAgICAgICAgICAgICAgIFRyaWdnZXJlZDoge"
"2Zvcm1hdFJlbGF0aXZlKGFsZXJ0LnRyaWdnZXJlZF9hdCl9CiAgICAgICAgICAgICAgICAg"
"ICAgICB7YWxlcnQuYWNrbm93bGVkZ2VkX2F0ICYmICgKICAgICAgICAgICAgICAgICAgICAg"
"ICAgPD57IiAifcK3eyIgIn1BY2s6IHtmb3JtYXRSZWxhdGl2ZShhbGVydC5hY2tub3dsZWRn"
"ZWRfYXQpfTwvPgogICAgICAgICAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgICAgICAg"
"ICAgIHthbGVydC5yZXNvbHZlZF9hdCAmJiAoCiAgICAgICAgICAgICAgICAgICAgICAgIDw+"
"eyIgIn3CtyB7IiAifVJlc29sdmVkOiB7Zm9ybWF0UmVsYXRpdmUoYWxlcnQucmVzb2x2ZWRf"
"YXQpfTwvPgogICAgICAgICAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgICAgICAgICA8"
"L3A+CiAgICAgICAgICAgICAgICAgICAge2FsZXJ0Lmxhc3RfZXJyb3IgJiYgKAogICAgICAg"
"ICAgICAgICAgICAgICAgPHAgY2xhc3NOYW1lPSJ0ZXh0LXNtIHRleHQtW2hzbCh2YXIoLS1m"
"b3JlZ3JvdW5kKSldIj4KICAgICAgICAgICAgICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1l"
"PSJmb250LW1lZGl1bSI+RXJyb3I6PC9zcGFuPiB7YWxlcnQubGFzdF9lcnJvcn0KICAgICAg"
"ICAgICAgICAgICAgICAgIDwvcD4KICAgICAgICAgICAgICAgICAgICApfQogICAgICAgICAg"
"ICAgICAgICA8L2Rpdj4KCiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4"
"IHNocmluay0wIGl0ZW1zLWNlbnRlciBnYXAtMS41Ij4KICAgICAgICAgICAgICAgICAgICB7"
"c3RhdHVzID09PSAiYWN0aXZlIiAmJiAoCiAgICAgICAgICAgICAgICAgICAgICA8QnV0dG9u"
"IHNpemU9InNtIiB2YXJpYW50PSJzZWNvbmRhcnkiIGRpc2FibGVkPXtpc011dGF0aW5nfQog"
"ICAgICAgICAgICAgICAgICAgICAgICBvbkNsaWNrPXsoKSA9PiBhY2tub3dsZWRnZU11dGF0"
"aW9uLm11dGF0ZShhbGVydC5pZCl9PgogICAgICAgICAgICAgICAgICAgICAgICBBY2tub3ds"
"ZWRnZQogICAgICAgICAgICAgICAgICAgICAgPC9CdXR0b24+CiAgICAgICAgICAgICAgICAg"
"ICAgKX0KICAgICAgICAgICAgICAgICAgICB7c3RhdHVzICE9PSAicmVzb2x2ZWQiICYmICgK"
"ICAgICAgICAgICAgICAgICAgICAgIDxCdXR0b24gc2l6ZT0ic20iIHZhcmlhbnQ9InNlY29u"
"ZGFyeSIgZGlzYWJsZWQ9e2lzTXV0YXRpbmd9CiAgICAgICAgICAgICAgICAgICAgICAgIG9u"
"Q2xpY2s9eygpID0+IHJlc29sdmVNdXRhdGlvbi5tdXRhdGUoYWxlcnQuaWQpfT4KICAgICAg"
"ICAgICAgICAgICAgICAgICAgUmVzb2x2ZQogICAgICAgICAgICAgICAgICAgICAgPC9CdXR0"
"b24+CiAgICAgICAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgICAgICAgICA8QnV0dG9u"
"IHNpemU9InNtIiB2YXJpYW50PSJnaG9zdCIgZGlzYWJsZWQ9e2lzTXV0YXRpbmd9CiAgICAg"
"ICAgICAgICAgICAgICAgICBvbkNsaWNrPXsoKSA9PiBkZWxldGVNdXRhdGlvbi5tdXRhdGUo"
"YWxlcnQuaWQpfT4KICAgICAgICAgICAgICAgICAgICAgIERlbGV0ZQogICAgICAgICAgICAg"
"ICAgICAgIDwvQnV0dG9uPgogICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAg"
"ICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgIHthbGVydC5sb2dfdGFpbCAmJiAoCiAgICAg"
"ICAgICAgICAgICAgIDxkaXY+CiAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbgogICAgICAg"
"ICAgICAgICAgICAgICAgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMSB0ZXh0"
"LXhzIHRleHQtW2hzbCh2YXIoLS1tdXRlZC1mb3JlZ3JvdW5kKSldIGhvdmVyOnRleHQtW2hz"
"bCh2YXIoLS1mb3JlZ3JvdW5kKSldIgogICAgICAgICAgICAgICAgICAgICAgb25DbGljaz17"
"KCkgPT4gc2V0RXhwYW5kZWRMb2dJZChsb2dFeHBhbmRlZCA/IG51bGwgOiBhbGVydC5pZCl9"
"CiAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAge2xvZ0V4cGFuZGVk"
"ID8gPENoZXZyb25Eb3duIGNsYXNzTmFtZT0iaDMgdzMiIC8+IDogPENoZXZyb25SaWdodCBj"
"bGFzc05hbWU9ImgzIHczIiAvPn0KICAgICAgICAgICAgICAgICAgICAgIEFnZW50IGxvZyB0"
"YWlsCiAgICAgICAgICAgICAgICAgICAgPC9idXR0b24+CiAgICAgICAgICAgICAgICAgICAg"
"e2xvZ0V4cGFuZGVkICYmICgKICAgICAgICAgICAgICAgICAgICAgIDxwcmUgY2xhc3NOYW1l"
"PSJtdC0xIG1heC1oLTQ4IG92ZXJmbG93LWF1dG8gcm91bmRlZCBiZy1baHNsKHZhcigtLW11"
"dGVkKSldLzUwIHAtMiB0ZXh0LXhzIGZvbnQtbW9ubyB0ZXh0LVtoc2wodmFyKC0tZm9yZWdy"
"b3VuZCkpXSI+CiAgICAgICAgICAgICAgICAgICAgICAgIHthbGVydC5sb2dfdGFpbH0KICAg"
"ICAgICAgICAgICAgICAgICAgIDwvcHJlPgogICAgICAgICAgICAgICAgICAgICl9CiAgICAg"
"ICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgICA8"
"L2Rpdj4KICAgICAgICAgICAgKTsKICAgICAgICAgIH0pfQogICAgICAgIDwvZGl2PgogICAg"
"ICA8L0NhcmRDb250ZW50PgogICAgPC9DYXJkPgogICk7Cn0K"
)
component = base64.b64decode(COMPONENT_B64).decode("utf-8")
print(f"Component decoded: {len(component)} chars")
insert_before = "export function Alerts() {"
if insert_before in content:
content = content.replace(insert_before, component + insert_before, 1)
print("OK: WatchdogAlertsSection inserted")
else:
print("ERROR: insert anchor not found")
sys.exit(1)
old_end = " </div>\n );\n}\n\n// Re-export types"
new_end = " <WatchdogAlertsSection />\n </div>\n );\n}\n\n// Re-export types"
if old_end in content:
content = content.replace(old_end, new_end, 1)
print("OK: WatchdogAlertsSection rendered in Alerts()")
else:
print("ERROR: closing div anchor not found")
idx = content.find("// Re-export types")
if idx >= 0:
print("Context:", repr(content[max(0, idx-200):idx+30]))
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Done. Lines: {len(content.splitlines())}")

File diff suppressed because one or more lines are too long

203
.claude/scripts/gen_b64.py Normal file
View File

@@ -0,0 +1,203 @@
"""Generate base64 of the WatchdogAlertsSection component."""
import base64, sys
BT = chr(96)
BULLET = "\xb7"
lines = [
"\n",
"// WatchdogAlertsSection\n",
"\n",
'type WdogStatus = "active" | "acknowledged" | "resolved";\n',
"\n",
"function wdogStatus(alert: WatchdogAlert): WdogStatus {\n",
' if (alert.resolved_at) return "resolved";\n',
' if (alert.acknowledged_at) return "acknowledged";\n',
' return "active";\n',
"}\n",
"\n",
"function WatchdogAlertsSection() {\n",
" const queryClient = useQueryClient();\n",
" const { toast } = useToast();\n",
" const [showAll, setShowAll] = useState(false);\n",
" const [expandedLogId, setExpandedLogId] = useState<string | null>(null);\n",
"\n",
" const { data: allAlerts = [], isLoading } = useQuery({\n",
' queryKey: ["watchdog-alerts"],\n',
" queryFn: () => watchdogAlertsApi.list().then((r) => r.data),\n",
" refetchInterval: 30000,\n",
" });\n",
"\n",
" const alerts = showAll ? allAlerts : allAlerts.filter((a) => !a.resolved_at);\n",
"\n",
" const acknowledgeMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts"] });\n',
' toast({ type: "success", title: "Alert acknowledged" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not acknowledge", message: err.message }),\n',
" });\n",
"\n",
" const resolveMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.resolve(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts"] });\n',
' toast({ type: "success", title: "Alert resolved" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not resolve", message: err.message }),\n',
" });\n",
"\n",
" const deleteMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.delete(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts"] });\n',
' toast({ type: "success", title: "Alert deleted" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not delete", message: err.message }),\n',
" });\n",
"\n",
" const isMutating =\n",
" acknowledgeMutation.isPending ||\n",
" resolveMutation.isPending ||\n",
" deleteMutation.isPending;\n",
"\n",
' const activeCount = allAlerts.filter((a) => wdogStatus(a) === "active").length;\n',
"\n",
" return (\n",
" <Card>\n",
" <CardHeader>\n",
' <div className="flex items-center justify-between gap-3">\n',
' <CardTitle className="flex items-center gap-2">\n',
' <AlertTriangle className="h-4 w-4 text-amber-500" />\n',
" Watchdog Alerts\n",
" {activeCount > 0 && (\n",
' <span className="ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">\n',
" {activeCount} active\n",
" </span>\n",
" )}\n",
" </CardTitle>\n",
" <button\n",
' className="text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] underline-offset-2 hover:underline"\n',
" onClick={() => setShowAll((v) => !v)}\n",
" >\n",
' {showAll ? "Active only" : "Show all"}\n',
" </button>\n",
" </div>\n",
" </CardHeader>\n",
" <CardContent>\n",
" {isLoading && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">Loading...</p>\n',
" )}\n",
" {!isLoading && alerts.length === 0 && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">\n',
' {showAll ? "No watchdog alerts recorded." : "No active watchdog alerts."}\n',
" </p>\n",
" )}\n",
' <div className="space-y-3">\n',
" {alerts.map((alert) => {\n",
" const status = wdogStatus(alert);\n",
" const logExpanded = expandedLogId === alert.id;\n",
" const statusColor =\n",
' status === "active"\n',
' ? "text-red-600 dark:text-red-400"\n',
' : status === "acknowledged"\n',
' ? "text-amber-600 dark:text-amber-400"\n',
' : "text-[hsl(var(--muted-foreground))]";\n',
"\n",
" return (\n",
" <div\n",
" key={alert.id}\n",
' className="rounded-lg border border-[hsl(var(--border))] p-4 space-y-2"\n',
" >\n",
' <div className="flex items-start justify-between gap-4">\n',
' <div className="space-y-0.5">\n',
' <div className="flex items-center gap-2">\n',
" <span className={" + BT + "text-xs font-semibold uppercase tracking-wider ${statusColor}" + BT + "}>\n",
" {status}\n",
" </span>\n",
' <span className="text-xs text-[hsl(var(--muted-foreground))]">\n',
" " + BULLET + " {alert.restart_attempts} restart attempt\n",
' {alert.restart_attempts !== 1 ? "s" : ""}\n',
" </span>\n",
" </div>\n",
' <p className="text-xs text-[hsl(var(--muted-foreground))]">\n',
' Agent ID:{" "}\n',
' <span className="font-mono" title={alert.agent_id}>\n',
" {alert.agent_id.slice(0, 8)}…\n",
" </span>\n",
' {" "}' + BULLET + '{" "}\n',
" Triggered: {formatRelative(alert.triggered_at)}\n",
" {alert.acknowledged_at && (\n",
' <>{" "}' + BULLET + '{" "}Ack: {formatRelative(alert.acknowledged_at)}</>\n',
" )}\n",
" {alert.resolved_at && (\n",
' <>{" "}' + BULLET + '{" "}Resolved: {formatRelative(alert.resolved_at)}</>\n',
" )}\n",
" </p>\n",
" {alert.last_error && (\n",
' <p className="text-sm text-[hsl(var(--foreground))]">\n',
' <span className="font-medium">Error:</span> {alert.last_error}\n',
" </p>\n",
" )}\n",
" </div>\n",
"\n",
' <div className="flex shrink-0 items-center gap-1.5">\n',
' {status === "active" && (\n',
' <Button size="sm" variant="secondary" disabled={isMutating}\n',
" onClick={() => acknowledgeMutation.mutate(alert.id)}>\n",
" Acknowledge\n",
" </Button>\n",
" )}\n",
' {status !== "resolved" && (\n',
' <Button size="sm" variant="secondary" disabled={isMutating}\n',
" onClick={() => resolveMutation.mutate(alert.id)}>\n",
" Resolve\n",
" </Button>\n",
" )}\n",
' <Button size="sm" variant="ghost" disabled={isMutating}\n',
" onClick={() => deleteMutation.mutate(alert.id)}>\n",
" Delete\n",
" </Button>\n",
" </div>\n",
" </div>\n",
"\n",
" {alert.log_tail && (\n",
" <div>\n",
" <button\n",
' className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"\n',
" onClick={() => setExpandedLogId(logExpanded ? null : alert.id)}\n",
" >\n",
' {logExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}\n',
" Agent log tail\n",
" </button>\n",
" {logExpanded && (\n",
' <pre className="mt-1 max-h-48 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]">\n',
" {alert.log_tail}\n",
" </pre>\n",
" )}\n",
" </div>\n",
" )}\n",
" </div>\n",
" );\n",
" })}\n",
" </div>\n",
" </CardContent>\n",
" </Card>\n",
" );\n",
"}\n",
"\n",
]
component = "".join(lines)
b64 = base64.b64encode(component.encode("utf-8")).decode("ascii")
with open("D:/claudetools/.claude/scripts/component.b64", "w") as f:
f.write(b64)
print(f"Component: {len(component)} chars")
print(f"Base64: {len(b64)} chars")
print("Written to component.b64")

View File

@@ -0,0 +1,346 @@
"""
Write replacement WatchdogAlerts.tsx that matches the new WatchdogAlert interface.
Run on the server: python3 /tmp/watchdog_alerts_page.py
"""
content = '''import { useState } from "react";
import { Link } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
ShieldAlert,
RefreshCw,
CheckCircle2,
Clock,
ChevronDown,
ChevronRight,
} from "lucide-react";
import { watchdogAlertsApi, WatchdogAlert } from "../api/client";
import { Card, CardHeader, CardTitle, CardContent } from "../components/Card";
import { Button } from "../components/Button";
import { NativeSelect } from "../components/Select";
import { useToast } from "../hooks/useToast";
import { cn } from "../lib/utils";
// ---------------------------------------------------------------------------
// Status derivation
// ---------------------------------------------------------------------------
type WatchdogStatus = "active" | "acknowledged" | "resolved";
function watchdogStatus(alert: WatchdogAlert): WatchdogStatus {
if (alert.resolved_at) return "resolved";
if (alert.acknowledged_at) return "acknowledged";
return "active";
}
// ---------------------------------------------------------------------------
// Status badge
// ---------------------------------------------------------------------------
function WatchdogStatusBadge({ status }: { status: WatchdogStatus }) {
const classes: Record<WatchdogStatus, string> = {
active: "bg-red-500/15 text-red-600 dark:text-red-400",
acknowledged: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
resolved: "bg-green-500/15 text-green-600 dark:text-green-400",
};
const label: Record<WatchdogStatus, string> = {
active: "Active",
acknowledged: "Acknowledged",
resolved: "Resolved",
};
return (
<span
className={cn(
"inline-block px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider",
classes[status]
)}
>
{label[status]}
</span>
);
}
// ---------------------------------------------------------------------------
// Filter type
// ---------------------------------------------------------------------------
type StatusFilter = "" | "active" | "acknowledged" | "resolved";
// ---------------------------------------------------------------------------
// WatchdogAlerts page
// ---------------------------------------------------------------------------
export function WatchdogAlerts() {
const { toast } = useToast();
const queryClient = useQueryClient();
const [statusFilter, setStatusFilter] = useState<StatusFilter>("active");
const [expandedLogId, setExpandedLogId] = useState<string | null>(null);
const {
data: allAlerts = [],
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["watchdog-alerts-page"],
queryFn: () => watchdogAlertsApi.list().then((r) => r.data),
refetchInterval: 30000,
});
// Client-side filter by derived status
const alerts = allAlerts.filter(
(a) => !statusFilter || watchdogStatus(a) === statusFilter
);
const acknowledgeMutation = useMutation({
mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
toast({ type: "success", title: "Alert acknowledged" });
},
onError: (err: Error) => {
toast({
type: "error",
title: "Could not acknowledge alert",
message: err.message,
});
},
});
const resolveMutation = useMutation({
mutationFn: (id: string) => watchdogAlertsApi.resolve(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
toast({ type: "success", title: "Alert resolved" });
},
onError: (err: Error) => {
toast({
type: "error",
title: "Could not resolve alert",
message: err.message,
});
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => watchdogAlertsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
toast({ type: "success", title: "Alert deleted" });
},
onError: (err: Error) => {
toast({ type: "error", title: "Could not delete alert", message: err.message });
},
});
const isMutating =
acknowledgeMutation.isPending ||
resolveMutation.isPending ||
deleteMutation.isPending;
const activeCount = allAlerts.filter((a) => watchdogStatus(a) === "active").length;
const acknowledgedCount = allAlerts.filter(
(a) => watchdogStatus(a) === "acknowledged"
).length;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Watchdog Alerts</h1>
<p className="text-[hsl(var(--muted-foreground))]">
Agent crash and restart exhaustion events
</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{/* Summary chips */}
<div className="flex flex-wrap gap-3">
<div className="flex items-center gap-3 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 min-w-[140px]">
<div className="text-2xl font-bold tabular-nums text-red-600 dark:text-red-400">
{activeCount}
</div>
<div className="text-xs uppercase tracking-wider text-red-600/80 dark:text-red-400/80">
Active
</div>
</div>
<div className="flex items-center gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-2 min-w-[140px]">
<div className="text-2xl font-bold tabular-nums text-amber-600 dark:text-amber-400">
{acknowledgedCount}
</div>
<div className="text-xs uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80">
Acknowledged
</div>
</div>
</div>
{isError && (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive">
Failed to load watchdog alerts. Check your connection and try
refreshing.
</div>
)}
<Card>
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-3">
<CardTitle>Alert Stream</CardTitle>
<NativeSelect
value={statusFilter}
onChange={(e) =>
setStatusFilter(e.target.value as StatusFilter)
}
className="w-auto min-w-[11rem]"
aria-label="Filter by status"
>
<option value="active">Active Only</option>
<option value="acknowledged">Acknowledged</option>
<option value="resolved">Resolved</option>
<option value="">All Statuses</option>
</NativeSelect>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="text-[hsl(var(--muted-foreground))]">
Loading watchdog alerts...
</p>
) : alerts.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<ShieldAlert className="h-10 w-10 text-[hsl(var(--muted-foreground))] mb-3" />
<p className="text-sm font-medium">No watchdog alerts</p>
<p className="text-xs text-[hsl(var(--muted-foreground))]">
No events match the current filter.
</p>
</div>
) : (
<div className="space-y-3">
{alerts.map((alert: WatchdogAlert) => {
const status = watchdogStatus(alert);
const logExpanded = expandedLogId === alert.id;
return (
<div
key={alert.id}
className="rounded-lg border border-[hsl(var(--border))] p-4 space-y-2"
>
<div className="flex items-start justify-between gap-4">
<div className="space-y-0.5 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<WatchdogStatusBadge status={status} />
<span className="text-xs text-[hsl(var(--muted-foreground))]">
{alert.restart_attempts} restart attempt
{alert.restart_attempts !== 1 ? "s" : ""}
</span>
</div>
<div className="text-xs text-[hsl(var(--muted-foreground))]">
<Link
to={`/agents/${alert.agent_id}`}
className="text-[hsl(var(--primary))] hover:underline font-mono"
>
{alert.agent_id.slice(0, 8)}…
</Link>
{" \xb7 "}
Triggered:{" "}
{new Date(alert.triggered_at).toLocaleString()}
{alert.acknowledged_at && (
<>
{" \xb7 "}Ack:{" "}
{new Date(alert.acknowledged_at).toLocaleString()}
{alert.acknowledged_by && ` by ${alert.acknowledged_by}`}
</>
)}
{alert.resolved_at && (
<>
{" \xb7 "}Resolved:{" "}
{new Date(alert.resolved_at).toLocaleString()}
</>
)}
</div>
{alert.last_error && (
<p className="text-sm text-[hsl(var(--foreground))] mt-1">
<span className="font-medium">Error:</span>{" "}
{alert.last_error}
</p>
)}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{status === "active" && (
<Button
variant="outline"
size="sm"
disabled={isMutating}
onClick={() =>
acknowledgeMutation.mutate(alert.id)
}
title="Acknowledge"
>
<Clock className="h-3.5 w-3.5 mr-1" />
Ack
</Button>
)}
{status !== "resolved" && (
<Button
size="sm"
disabled={isMutating}
onClick={() => resolveMutation.mutate(alert.id)}
title="Resolve"
>
<CheckCircle2 className="h-3.5 w-3.5 mr-1" />
Resolve
</Button>
)}
<Button
variant="ghost"
size="sm"
disabled={isMutating}
onClick={() => deleteMutation.mutate(alert.id)}
>
Delete
</Button>
</div>
</div>
{alert.log_tail && (
<div>
<button
className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"
onClick={() =>
setExpandedLogId(logExpanded ? null : alert.id)
}
>
{logExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
Agent log tail
</button>
{logExpanded && (
<pre className="mt-1 max-h-64 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]">
{alert.log_tail}
</pre>
)}
</div>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
);
}
'''
path = "/home/guru/gururmm/dashboard/src/pages/WatchdogAlerts.tsx"
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Written WatchdogAlerts.tsx: {len(content.splitlines())} lines")

View File

@@ -0,0 +1,508 @@
import sys
path = "/home/guru/gururmm/dashboard/src/pages/Alerts.tsx"
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# ─── Insert WatchdogAlertsSection component before export function Alerts() ───
insert_before = "export function Alerts() {"
watchdog_section = (
"// ─── WatchdogAlertsSection ─────────────────────────────────────────────────\n"
"\n"
"type WdogStatus = \"active\" | \"acknowledged\" | \"resolved\";\n"
"\n"
"function wdogStatus(alert: WatchdogAlert): WdogStatus {\n"
" if (alert.resolved_at) return \"resolved\";\n"
" if (alert.acknowledged_at) return \"acknowledged\";\n"
" return \"active\";\n"
"}\n"
"\n"
"function WatchdogAlertsSection() {\n"
" const queryClient = useQueryClient();\n"
" const { toast } = useToast();\n"
" const [showAll, setShowAll] = useState(false);\n"
" const [expandedLogId, setExpandedLogId] = useState<string | null>(null);\n"
"\n"
" const { data: allAlerts = [], isLoading } = useQuery({\n"
" queryKey: [\"watchdog-alerts\"],\n"
" queryFn: () => watchdogAlertsApi.list().then((r) => r.data),\n"
" refetchInterval: 30000,\n"
" });\n"
"\n"
" const alerts = showAll\n"
" ? allAlerts\n"
" : allAlerts.filter((a) => !a.resolved_at);\n"
"\n"
" const acknowledgeMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert acknowledged\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not acknowledge\", message: err.message }),\n"
" });\n"
"\n"
" const resolveMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.resolve(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert resolved\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not resolve\", message: err.message }),\n"
" });\n"
"\n"
" const deleteMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.delete(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert deleted\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not delete\", message: err.message }),\n"
" });\n"
"\n"
" const isMutating =\n"
" acknowledgeMutation.isPending ||\n"
" resolveMutation.isPending ||\n"
" deleteMutation.isPending;\n"
"\n"
" const activeCount = allAlerts.filter((a) => wdogStatus(a) === \"active\").length;\n"
"\n"
" return (\n"
" <Card>\n"
" <CardHeader>\n"
" <div className=\"flex items-center justify-between gap-3\">\n"
" <CardTitle className=\"flex items-center gap-2\">\n"
" <AlertTriangle className=\"h-4 w-4 text-amber-500\" />\n"
" Watchdog Alerts\n"
" {activeCount > 0 && (\n"
" <span className=\"ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400\">\n"
" {activeCount} active\n"
" </span>\n"
" )}\n"
" </CardTitle>\n"
" <button\n"
" className=\"text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] underline-offset-2 hover:underline\"\n"
" onClick={() => setShowAll((v) => !v)}\n"
" >\n"
" {showAll ? \"Active only\" : \"Show all\"}\n"
" </button>\n"
" </div>\n"
" </CardHeader>\n"
" <CardContent>\n"
" {isLoading && (\n"
" <p className=\"py-6 text-center text-sm text-[hsl(var(--muted-foreground))]\">Loading...</p>\n"
" )}\n"
" {!isLoading && alerts.length === 0 && (\n"
" <p className=\"py-6 text-center text-sm text-[hsl(var(--muted-foreground))]\">\n"
" {showAll ? \"No watchdog alerts recorded.\" : \"No active watchdog alerts.\"}\n"
" </p>\n"
" )}\n"
" <div className=\"space-y-3\">\n"
" {alerts.map((alert) => {\n"
" const status = wdogStatus(alert);\n"
" const logExpanded = expandedLogId === alert.id;\n"
" const statusColor =\n"
" status === \"active\"\n"
" ? \"text-red-600 dark:text-red-400\"\n"
" : status === \"acknowledged\"\n"
" ? \"text-amber-600 dark:text-amber-400\"\n"
" : \"text-[hsl(var(--muted-foreground))]\";\n"
"\n"
" return (\n"
" <div\n"
" key={alert.id}\n"
" className=\"rounded-lg border border-[hsl(var(--border))] p-4 space-y-2\"\n"
" >\n"
" <div className=\"flex items-start justify-between gap-4\">\n"
" <div className=\"space-y-0.5\">\n"
" <div className=\"flex items-center gap-2\">\n"
" <span\n"
" className={`text-xs font-semibold uppercase tracking-wider ${statusColor}`}\n"
" >\n"
" {status}\n"
" </span>\n"
" <span className=\"text-xs text-[hsl(var(--muted-foreground))]\">\n"
" \xb7 {alert.restart_attempts} restart attempt\n"
" {alert.restart_attempts !== 1 ? \"s\" : \"\"}\n"
" </span>\n"
" </div>\n"
" <p className=\"text-xs text-[hsl(var(--muted-foreground))]\">\n"
" Agent:{\" \"}\n"
" <span\n"
" className=\"font-mono\"\n"
" title={alert.agent_id}\n"
" >\n"
" {alert.agent_id.slice(0, 8)}…\n"
" </span>\n"
" {\" \"}\xb7{\" \"}\n"
" Triggered: {formatRelative(alert.triggered_at)}\n"
" {alert.acknowledged_at && (\n"
" <>\n"
" {\" \"}\xb7{\" \"}Ackd:{\" \"}\n"
" {formatRelative(alert.acknowledged_at)}\n"
" </>\n"
" )}\n"
" {alert.resolved_at && (\n"
" <>\n"
" {\" \"}\xb7{\" \"}Resolved:{\" \"}\n"
" {formatRelative(alert.resolved_at)}\n"
" </>\n"
" )}\n"
" </p>\n"
" {alert.last_error && (\n"
" <p className=\"text-sm text-[hsl(var(--foreground))]\">\n"
" <span className=\"font-medium\">Error:</span> {alert.last_error}\n"
" </p>\n"
" )}\n"
" </div>\n"
"\n"
" <div className=\"flex shrink-0 items-center gap-1.5\">\n"
" {status === \"active\" && (\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"secondary\"\n"
" disabled={isMutating}\n"
" onClick={() => acknowledgeMutation.mutate(alert.id)}\n"
" >\n"
" Acknowledge\n"
" </Button>\n"
" )}\n"
" {status !== \"resolved\" && (\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"secondary\"\n"
" disabled={isMutating}\n"
" onClick={() => resolveMutation.mutate(alert.id)}\n"
" >\n"
" Resolve\n"
" </Button>\n"
" )}\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"ghost\"\n"
" disabled={isMutating}\n"
" onClick={() => deleteMutation.mutate(alert.id)}\n"
" >\n"
" Delete\n"
" </Button>\n"
" </div>\n"
" </div>\n"
"\n"
" {alert.log_tail && (\n"
" <div>\n"
" <button\n"
" className=\"flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]\"\n"
" onClick={() =>\n"
" setExpandedLogId(logExpanded ? null : alert.id)\n"
" }\n"
" >\n"
" {logExpanded ? (\n"
" <ChevronDown className=\"h-3 w-3\" />\n"
" ) : (\n"
" <ChevronRight className=\"h-3 w-3\" />\n"
" )}\n"
" Agent log tail\n"
" </button>\n"
" {logExpanded && (\n"
" <pre className=\"mt-1 max-h-48 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]\">\n"
" {alert.log_tail}\n"
" </pre>\n"
" )}\n"
" </div>\n"
" )}\n"
" </div>\n"
" );\n"
" })}\n"
" </div>\n"
" </CardContent>\n"
" </Card>\n"
" );\n"
"}\n"
"\n"
)
if insert_before in content:
content = content.replace(insert_before, watchdog_section + insert_before, 1)
print("OK: WatchdogAlertsSection inserted")
else:
print("ERROR: could not find 'export function Alerts() {'")
sys.exit(1)
# ─── Add <WatchdogAlertsSection /> at the bottom of the Alerts return div ───
# The closing of the Alerts function return div
old_end = " </div>\n );\n}\n\n// Re-export types"
new_end = " <WatchdogAlertsSection />\n </div>\n );\n}\n\n// Re-export types"
if old_end in content:
content = content.replace(old_end, new_end, 1)
print("OK: <WatchdogAlertsSection /> rendered in Alerts return")
else:
print("ERROR: closing div anchor not found")
# Debug
idx = content.find("// Re-export types")
if idx >= 0:
print("Context:", repr(content[max(0,idx-200):idx+30]))
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Done. Lines: {len(content.splitlines())}")
"\n",
'type WatchdogStatus = "active" | "acknowledged" | "resolved";\n',
"\n",
"function watchdogStatus(alert: WatchdogAlert): WatchdogStatus {\n",
' if (alert.resolved_at) return "resolved";\n',
' if (alert.acknowledged_at) return "acknowledged";\n',
' return "active";\n',
"}\n",
"\n",
"function WatchdogAlertsPanel({ agentId }: { agentId: string }) {\n",
" const queryClient = useQueryClient();\n",
" const { toast } = useToast();\n",
' const [statusFilter, setStatusFilter] = useState<"" | WatchdogStatus>("");\n',
" const [expandedLogId, setExpandedLogId] = useState<string | null>(null);\n",
"\n",
" const { data: alerts = [], isLoading } = useQuery({\n",
' queryKey: ["watchdog-alerts", agentId],\n',
" queryFn: () => watchdogAlertsApi.listForAgent(agentId).then((r) => r.data),\n",
" refetchInterval: 30000,\n",
" });\n",
"\n",
" const filtered = alerts.filter(\n",
" (a) => !statusFilter || watchdogStatus(a) === statusFilter\n",
" );\n",
"\n",
" const acknowledgeMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert acknowledged" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not acknowledge", message: err.message }),\n',
" });\n",
"\n",
" const resolveMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.resolve(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert resolved" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not resolve", message: err.message }),\n',
" });\n",
"\n",
" const deleteMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.delete(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert deleted" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not delete", message: err.message }),\n',
" });\n",
"\n",
" const isMutating =\n",
" acknowledgeMutation.isPending ||\n",
" resolveMutation.isPending ||\n",
" deleteMutation.isPending;\n",
"\n",
' const activeCount = alerts.filter((a) => watchdogStatus(a) === "active").length;\n',
"\n",
" return (\n",
" <Card>\n",
" <CardHeader>\n",
' <CardTitle className="flex items-center gap-2">\n',
' <AlertTriangle className="h-4 w-4 text-amber-500" />\n',
" Watchdog Alerts\n",
" {activeCount > 0 && (\n",
' <span className="ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">\n',
" {activeCount} active\n",
" </span>\n",
" )}\n",
" </CardTitle>\n",
" </CardHeader>\n",
' <CardContent className="space-y-3">\n',
' <div className="flex items-center gap-2">\n',
" <NativeSelect\n",
" value={statusFilter}\n",
' onChange={(e) => setStatusFilter(e.target.value as "" | WatchdogStatus)}\n',
' className="w-auto min-w-[10rem]"\n',
' aria-label="Filter by status"\n',
" >\n",
' <option value="">All Statuses</option>\n',
' <option value="active">Active</option>\n',
' <option value="acknowledged">Acknowledged</option>\n',
' <option value="resolved">Resolved</option>\n',
" </NativeSelect>\n",
" </div>\n",
"\n",
" {isLoading && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">\n',
" Loading...\n",
" </p>\n",
" )}\n",
"\n",
" {!isLoading && filtered.length === 0 && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">\n',
" {statusFilter\n",
' ? "No alerts match the current filter."\n',
' : "No watchdog alerts recorded."}\n',
" </p>\n",
" )}\n",
"\n",
" {filtered.map((alert) => {\n",
" const status = watchdogStatus(alert);\n",
" const logExpanded = expandedLogId === alert.id;\n",
" const statusColor =\n",
' status === "active"\n',
' ? "text-red-600 dark:text-red-400"\n',
' : status === "acknowledged"\n',
' ? "text-amber-600 dark:text-amber-400"\n',
' : "text-[hsl(var(--muted-foreground))]";\n',
"\n",
" return (\n",
" <div\n",
" key={alert.id}\n",
' className="rounded-lg border border-[hsl(var(--border))] p-4 space-y-2"\n',
" >\n",
' <div className="flex items-start justify-between gap-4">\n',
' <div className="space-y-0.5">\n',
' <div className="flex items-center gap-2">\n',
" <span\n",
" className={`text-xs font-semibold uppercase tracking-wider ${statusColor}`}\n",
" >\n",
" {status}\n",
" </span>\n",
' <span className="text-xs text-[hsl(var(--muted-foreground))]">\n',
" \xb7 {alert.restart_attempts} restart attempt\n",
' {alert.restart_attempts !== 1 ? "s" : ""}\n',
" </span>\n",
" </div>\n",
' <p className="text-xs text-[hsl(var(--muted-foreground))]">\n',
" Triggered: {new Date(alert.triggered_at).toLocaleString()}\n",
" {alert.acknowledged_at && (\n",
" <>\n",
' {" "}\n',
" \xb7 Acknowledged:{\" \"}\n",
" {new Date(alert.acknowledged_at).toLocaleString()}\n",
" </>\n",
" )}\n",
" {alert.resolved_at && (\n",
" <>\n",
' {" "}\n',
" \xb7 Resolved:{\" \"}\n",
" {new Date(alert.resolved_at).toLocaleString()}\n",
" </>\n",
" )}\n",
" </p>\n",
" {alert.last_error && (\n",
' <p className="text-sm text-[hsl(var(--foreground))]">\n',
' <span className="font-medium">Error:</span> {alert.last_error}\n',
" </p>\n",
" )}\n",
" </div>\n",
"\n",
' <div className="flex shrink-0 items-center gap-1.5">\n',
' {status === "active" && (\n',
" <Button\n",
' size="sm"\n',
' variant="secondary"\n',
" disabled={isMutating}\n",
" onClick={() => acknowledgeMutation.mutate(alert.id)}\n",
" >\n",
" Acknowledge\n",
" </Button>\n",
" )}\n",
' {status !== "resolved" && (\n',
" <Button\n",
' size="sm"\n',
' variant="secondary"\n',
" disabled={isMutating}\n",
" onClick={() => resolveMutation.mutate(alert.id)}\n",
" >\n",
" Resolve\n",
" </Button>\n",
" )}\n",
" <Button\n",
' size="sm"\n',
' variant="ghost"\n',
" disabled={isMutating}\n",
" onClick={() => deleteMutation.mutate(alert.id)}\n",
" >\n",
" Delete\n",
" </Button>\n",
" </div>\n",
" </div>\n",
"\n",
" {alert.log_tail && (\n",
" <div>\n",
" <button\n",
' className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"\n',
" onClick={() =>\n",
" setExpandedLogId(logExpanded ? null : alert.id)\n",
" }\n",
" >\n",
" {logExpanded ? (\n",
' <ChevronDown className="h-3 w-3" />\n',
" ) : (\n",
' <ChevronRight className="h-3 w-3" />\n',
" )}\n",
" Agent log tail\n",
" </button>\n",
" {logExpanded && (\n",
' <pre className="mt-1 max-h-48 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]">\n',
" {alert.log_tail}\n",
" </pre>\n",
" )}\n",
" </div>\n",
" )}\n",
" </div>\n",
" );\n",
" })}\n",
" </CardContent>\n",
" </Card>\n",
" );\n",
"}\n",
"\n",
]
file_lines[anchor_idx:anchor_idx] = C
print(f"OK: inserted {len(C)} lines before AgentAlertsPanel anchor")
content2 = "".join(file_lines)
old_tab = (
' <TabPanel tabId="alerts" activeTab={activeTab}>\n'
" <AgentAlertsPanel agentId={agent.id} />\n"
" </TabPanel>"
)
new_tab = (
' <TabPanel tabId="alerts" activeTab={activeTab}>\n'
" <AgentAlertsPanel agentId={agent.id} />\n"
' <div className="mt-6">\n'
" <WatchdogAlertsPanel agentId={agent.id} />\n"
" </div>\n"
" </TabPanel>"
)
if old_tab in content2:
content2 = content2.replace(old_tab, new_tab, 1)
print("OK: TabPanel updated")
else:
print("ERROR: TabPanel anchor not found")
idx = content2.find('tabId="alerts"')
if idx >= 0:
print("Context:", repr(content2[idx:idx+200]))
with open(path, "w", encoding="utf-8") as f:
f.write(content2)
print(f"Done. Lines: {len(content2.splitlines())}")

View File

@@ -182,3 +182,62 @@ Medical breakthroughs preserved as filler content:
**HTML File Location:** `file:///Users/azcomputerguru/ClaudeTools/projects/radio-show/episodes/2026-05-23-show/show-prep.html`
**For Howard:** Open in browser to review full show prep with color-coded sections, talking points, sources, and transitions.
---
## Update: 01:20 PT — GuruRMM / Paul Key / Windows Update roadmap
**Machine:** DESKTOP-0O8A1RL
### Session Summary
Completed three work items on the desktop workstation in the early hours of 2026-05-23.
Added Paul Key as a new GuruRMM client with a "Home" site. Used the GuruRMM API directly (login → `POST /api/clients``POST /api/sites`) to create the client and site. Site enrollment key received and vaulted at `clients/key-paul/gururmm-site-home.sops.yaml` with SOPS age encryption. Vault committed and pushed.
Diagnosed KEY-MEDIA (Paul Key's Windows 11 media server — i5-13420H, 15.6 GB RAM, agent 0.6.28, already enrolled and online at `10.0.0.100`). Ran three rounds of remote PowerShell diagnostics via GuruRMM command API in system context (headless machine, no user session). Found three issues: (1) recurring Kernel-Power 41 unclean shutdowns — three events over six months (11/11/2025, 01/22/2026, 05/22/2026), no BSODs or minidumps, machine was down ~4 hours on 05/22 — power loss pattern, needs UPS; (2) Ombi misconfigured with wrong Plex port — `PlexContentSync` targeting `10.0.0.100:10363` but Plex actually listens on `32400`; (3) pending reboot from six `PendingFileRenameOperations` entries. Disk health (C: 89% free, D: 4.6 TB media drive at 81% free, both Healthy SMART), memory (9 GB free), and running media stack (Plex, Sonarr, Radarr, SABnzbd, Ombi) were all clean.
Added comprehensive Windows Update Management feature spec to `docs/FEATURE_ROADMAP.md`. Three operating modes: Monitor (passive, alerts only, user keeps WU control), Semi-Controlled (we own schedule/approval, user can still interact), Fully Managed (WU locked via registry/GP, no user access). Full stack documented: agent Windows WUA COM API with blacklist via `IUpdate.IsHidden` and real-time progress reporting; server with five new tables and approval/denial/blacklist endpoints; dashboard with per-agent WU tab, site fleet queue, policy editor, blacklist manager; approval workflow with auto-approve by severity threshold. "Patch Now" marked P1.
Also answered a support question: Claude Code appearing to pause mid-task (timer freezes, everything catches up on Enter) is Windows Terminal selection mode — any click in the terminal buffers stdout until Enter/Escape; the process is running normally the whole time.
### Key Decisions
- All KEY-MEDIA diagnostics in system context — headless media server, user-session context would fail with no active session error.
- Three staged diagnostic rounds rather than one large script — easier to handle JSON escaping failures and isolate issues.
- Paul Key vault entry matches existing client GuruRMM site format (same structure as cascades-tucson, imc, kittle, stamback-septic).
- "Patch Now" marked P1 — techs need immediate install path during incidents; approval/scheduling workflow is secondary.
### Problems Encountered
- **JSON escaping in PowerShell-over-curl payload.** Multi-line PowerShell script in shell heredoc caused `jq` parse errors. Resolution: Python `json.dumps()` to write payload to `D:/claudetools/.claude/tmp_cmd_payload.json`, then `curl --data-binary @file`. Pattern reused for all three diagnostic rounds.
- **Session log merge conflict.** Mac session had already written `session-logs/2026-05-23-session.md` for the radio show. Desktop session created the same file. Resolved by aborting rebase, stashing staged scripts, fast-forward pulling the Mac session, then appending Desktop session as an Update section.
### Configuration Changes
- **CREATED** `D:/vault/clients/key-paul/gururmm-site-home.sops.yaml` — SOPS-encrypted enrollment key for Paul Key Home site. Vault commit `4df0c9c`.
- **MODIFIED** `D:/claudetools/projects/msp-tools/guru-rmm/docs/FEATURE_ROADMAP.md` — Added Windows Update Management section (~100 lines). Replaced single `[ ] Windows Update status - P2` bullet. Updated last-updated to 2026-05-23.
### Credentials & Secrets
**Paul Key — GuruRMM Home Site**
- Enrollment key: `grmm_EvOPzz6kCP99m5jyBuDBmGwqR4Y-I3f7`
- Vault: `clients/key-paul/gururmm-site-home.sops.yaml`
- Client ID: `9a669d23-02c8-4772-8577-fa84355361fd`
- Site ID: `a5b237db-5198-45af-8747-1fdf3aef445d`
- Site code: `IRON-WOLF-5819`
- Note: Key shown once at creation. Will not be returned by API again.
### Pending / Incomplete Tasks
- **KEY-MEDIA — fix Ombi port:** Change Plex URL in Ombi from `10.0.0.100:10363` to `10.0.0.100:32400`. Requires Ombi web UI access (likely `http://10.0.0.100:5000` — confirm with Paul).
- **KEY-MEDIA — reboot:** Six `PendingFileRenameOperations` pending. Schedule maintenance reboot via GuruRMM.
- **KEY-MEDIA — UPS advisory:** Three power events over six months. Recommend UPS to Paul.
- **GuruRMM Windows Update module:** Spec complete in roadmap. No implementation started. Use `/shape-spec` when prioritized.
### Reference
- KEY-MEDIA agent ID: `8c12d038-a017-422b-84ef-dd284188e146`
- Plex listen: `:::32400` (confirmed via `Get-NetTCPConnection`)
- Ombi path: `D:\Ombi\Ombi.exe`
- GuruRMM roadmap: `projects/msp-tools/guru-rmm/docs/FEATURE_ROADMAP.md`