From 0ecec5f1121894be438a0106021ef3beced24bb8 Mon Sep 17 00:00:00 2001 From: christian Date: Sun, 25 Oct 2026 07:19:51 -0400 Subject: [PATCH] ABLESET switch (Restart page): hold to stop AbleSet being watched and opened The user has an AbleSet bug and needs auto-open off for now. A bypass key can now hold off fixes (bypass.fixes): their checks stay green and nothing runs them. The status page is full, so a Restart page key with toggle shows the switch and its hold flips it. Switched off live 2026-10-25 07:18. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- config.json | 22 ++++++++++++++++++++++ make_page.py | 17 ++++++++++++----- restart-page.companionconfig | Bin 3906 -> 4093 bytes schema.py | 21 ++++++++++++++++----- status_panel.py | 26 ++++++++++++++++++++++---- tests/test_status_panel.py | 20 ++++++++++++++++++++ 7 files changed, 93 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1c35faf..a792dfb 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The **page number key** on every page follows the ALL button. It's green when ev | LED | VX1000 and UHD Jr: Companion connection **and** each one answers ping (the NovaStar module keeps saying "Connected" after a processor is switched off). "UHDJR NO PING" = processor off or unreachable; "UHDJR OFFLINE" = Companion's connection failed; "UHDJR DISABLED" (amber) = connection turned off in Companion. | | DANTE | All 19 Dante devices ("19/19"), named when one drops: "WING GONE", "TRACKS GONE", "PROP MAC GONE". | | MIDI | All six network MIDI links on the FOH / Studio B checklist, each named when it drops: **LIGHT PC** (Ingest → Lighting), **SUPERRACK** (Ingest → Livebox), **FX LIGHTS** (Tracks → Lighting, the FX lighting cues), **TRK LIVEBOX** (Tracks → Livebox), **TRK STUDIO B** (Tracks → Studio B), **PROP LIGHTS** (ProPresenter → Lighting). NA Ingest's own session is read locally; NA Tracks' and NA ProP's over SSH every 5 s. Also: the Lighting PC and NA Tracks MIDI sessions still announced on the network. A clean disconnect shows within about 10 s; a crash or pulled cable takes about a minute (macOS's own timeout, measured). Hold 2 s reconnects whichever links dropped. | -| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC; **AbleSet** running on the Tracks Mac (ProdDash shows it): **ABLESET NOT RUNNING**, and the hold (and auto-fix) opens it in the background and minimizes its window (`mac_start` with *Minimized*). Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections.; **Pro Tools on the Studio B Mac Studio** set to Solo Mode **AFL + Latch** ("PT SOLO NOT AFL"; hold, or Sunday auto-fix, ticks them back). A helper on that Mac (`remote-access/solo-mode-agent.swift`, installed with `install-solo-mode.sh`) reads the menu; it needs Accessibility allowed once, then a restart of the helper. "PT SOLO CHECK OFF" = that helper is not running or not allowed. | +| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC; **AbleSet** running on the Tracks Mac (ProdDash shows it): **ABLESET NOT RUNNING**, and the hold (and auto-fix) opens it in the background and minimizes its window (`mac_start` with *Minimized*). While **ABLESET** on the Restart page (row 1 col 6) says **AUTO-OPEN OFF**, AbleSet is neither checked nor opened (hold it to switch; it stays as left, through restarts: a `bypass` key with `fixes`, shown by a Restart page key with `toggle`). Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections.; **Pro Tools on the Studio B Mac Studio** set to Solo Mode **AFL + Latch** ("PT SOLO NOT AFL"; hold, or Sunday auto-fix, ticks them back). A helper on that Mac (`remote-access/solo-mode-agent.swift`, installed with `install-solo-mode.sh`) reads the menu; it needs Accessibility allowed once, then a restart of the helper. "PT SOLO CHECK OFF" = that helper is not running or not allowed. | | VISTA | Vista on the Lighting PC: its Art-Net poll (sent every 0.5 s) must be heard within 5 s, otherwise **SILENT** (Vista closed or crashed, or the Lighting PC is off the network). Hold 2 s starts Vista if it is not running; Vista then opens **Prop Takeover** from its show folder (`E:\media\Documents\Vista 3\Shows`) by itself, through its own autoload preference (`` in `E:\media\Documents\Vista 3\.preferences`, set 2026-10-08). Resolume is the ARENA button. | | RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. | | NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21). A blip **flashes amber** for 5 minutes, then goes by itself; press NET to stop the flashing, hold NET 2 s to clear it now. | diff --git a/config.json b/config.json index e558d90..e75e1dd 100644 --- a/config.json +++ b/config.json @@ -1697,6 +1697,16 @@ "watch": [ "conn:arena" ] + }, + { + "id": "rs_ableset", + "label": "ABLESET", + "fix": "ableset", + "toggle": "ableset", + "pos": [ + 1, + 6 + ] } ], "page": 20, @@ -2657,6 +2667,18 @@ 3, 7 ] + }, + { + "id": "ableset", + "label": "ABLESET", + "kind": "bypass", + "bypass": { + "fixes": [ + "ableset" + ], + "on_text": "AUTO-OPEN\nOFF", + "off_text": "AUTO-OPEN\nON" + } } ], "timing": { diff --git a/make_page.py b/make_page.py index 9d77517..8f858ed 100644 --- a/make_page.py +++ b/make_page.py @@ -150,17 +150,22 @@ def restart_key(key, cfg): names, opts = cfg["variables"], settings(cfg) hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0) stale_text = "'%s\\n?'" % key["label"] + if key.get("toggle"): # a switch kept on this page (ABLESET): its hold flips it + verb, note = "restart %s $(internal:time_unix)" % key["toggle"], ( + "SWITCH key '%s' - hold %s s: switch %s off or back on (status_panel.py on %s)." % (key["id"], secs, key["label"], opts["ingest"])) + else: + verb, note = "force %s $(internal:time_unix)" % key["id"], ( + "RESTART key '%s' - hold %s s: restart it even when the status page says it is fine " + "(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"])) return { "type": "button-layered", "style": {"layers": layers(key["id"], key, names, stale_text, opts)}, "options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True, - "notes": "RESTART key '%s' - hold %s s: restart it even when the status page says it is fine " - "(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"])}, + "notes": note}, "feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": [], - str(hold_ms): [command_action(key["id"], "hold", names["command"], - "force %s $(internal:time_unix)" % key["id"])]}, + str(hold_ms): [command_action(key["id"], "hold", names["command"], verb)]}, "options": {"runWhileHeld": [hold_ms]}}}, "localVariables": [], } @@ -214,7 +219,9 @@ def button(tid, tile, cfg): stale_text = "'%s\\n?'" % label press, hold = "ack " + tid, "restart " + tid b = tile.get("bypass") or {} - note = ("Hold %s s: hand %s to an operator - its MIDI links (%s) are taken out%s and the panel stops putting them " + held = ", ".join((cfg.get("fixes", {}).get(f) or {}).get("short", f) for f in b.get("fixes") or []) + note = ("Hold %s s: switch off %s - its checks stay green and nothing opens or fixes it. Hold again: back on." % (secs, held) + if held and not b.get("peers") else None) or ("Hold %s s: hand %s to an operator - its MIDI links (%s) are taken out%s and the panel stops putting them " "back. Hold again: everything is put back." % (secs, label, ", ".join(b.get("peers") or []), " and Companion's %s switched off" % ", ".join(b["conns"]) if b.get("conns") else "")) else: diff --git a/restart-page.companionconfig b/restart-page.companionconfig index c659c0cca22e010381cbcf63b3339326164eead3..fb8366666044000657d5bf9338184b0e9b42671a 100644 GIT binary patch literal 4093 zcmVbYXIIEpTCHWiDfHZE#_3X>V?0Z*FF3X8`S;X>;02 z7KXq3SLmuz*LF?pVqZLyxrH&o*aR?=*YPb%7A=r1BrJ)|x%uy-6|)Ht#!9TbUlNRZ zp<739pYzat_`XBYeWGfjBGX4yH1^r}TOW0a@dw@IT7)*=;FV67lZ_(BJyB|!my(B( z!}Pzh=~Vnc)42zU9PosX37HH@304)W?h*BUhoWfFsv@V0#}T&u(vitU;q;R2WaBTI zS=PA3b*v6!k^6hOPK*{+-M+#rvaTwU<}^jo5%<;P%ju8Y>(bV{KR(;#q6^AB^mJX3 z4O>vmlWB!vS0$QrTLn(i$oTS)iPp8YGGKI0 z?`b7tsx_}Jnnd7wlJ29Aow?5DChkEQMix!mF~>i(RHY|3eNCOw+~nItHqEl>F(O^< zk&Pxy>2kcNk35=+a_YGt#7$eRneC{C!00OE0Usm zJrajpn_n`~Z)vxtXyP*U6lG0plI<#+*fF$fp|+Je z&YHTmtckNVAZ!q}Z9E;EMp6me9v8PO_atevN#G>0<@vzz#MI2q+?D>jwJeB|^i64c z&4qD&%ifPphWbK@V2umD6hfb=0D*u&Y+>}4S#8Jh**&>ps9L>RGs3&Sc6~Z5!`;o? zm2G-%(NsE}ju|hv=(-8MT{mH4*X=_pvJv`g;tSc*EsRBc&bGFp-MODTCrL(Z-Bv~U zY1>k4cHueYE~`1vI9c0l>Xn`@i85K{k|$#6Of(ybWFi}^iq4C_ccywaRv1&nCLsi^ z>7-jzmDX#U)61Y$V5(30^_nS*>L|y%ty&*ve3j>9W5E}N5o1E1lAP;`+RRO%Wko0E zn{>Uvu(>)*J&_T$J7FcehQYNJsY%5s?TAusbVZ3eMfyah25sXXy-g_1LM)U?rznjZ zX=VvgqXc7;sXhB*!?Na;h4y$THzy ztGPNcU25Liru?v}Hz%GiLooPGbE>IThClsuO}6IM$)r3Pz3u;7P*k2!6NX1Elt14e zYscdw(tKN#nkuoBeWKLs%fIJ++=Ww08y0+RG)7K|g^ni-#x$V??d?qcKQ?kn;W_Eb zx%IZCPhs8|FfTAKf*k}q;ljMSYA)>lK(AW|s28Xg;s(?U)C<%Lfq*~+g?ia~E`QGW zguRZ_5<=YTj7-?Tg#+>p2>Ckm4d@H#yA=A`b`mi5bvg|R_$7Q(Y4+JZe{Wg2Jqim{+j^*1^-0^ga{~H{8zLb0)PD1tpolG{tIyf{{{aA z|Ajz6AcEq*rAo2Dus->(z@&?2EMW) zlXu0vn|ClBsH_nJU6!ilZt`&;_4wx3Sf-p$o#l!Psqi%31rA0)1RQMoshqU#CSa>1 z(C@pWUw&}%$Z0Cmy!iUkyEv#FW?qfy_GEgo;FL*H(?d{RLR}b8<`SR^W9aG-t$tU zg({(vj^fIM5l>M6q2dwq)~_rq@0xlR**EpfjH->nY-AGeqSo4Tk5i4n)ldR2UuP-` z^tTkedc$5Jk1YpxkL~MTtJ4|s*{t+bJH2Rk2{RpecyQk(ir~$Dy+py=-B}$J{FsT8 z7CgS52JN@?p=Y~qX?652tuA=1w>A%p9`CI+zW8z5(yYu=<7qExW;LPT(A5He>=(eh zrlTdS?UK!*+!eApXDAD>>vHhwcb5C_-os+RJJClxvW3s;z5dA{>r>p%aja(ur`IKD>d`6j-= z@rC0H$9Fx)H&(A?`sC~`c9t)dBiz~XLFK#!#}|(8n;hT72labvxncpX?nifZ9~9jf z+Pk{CiRfW032*nOc)OpME@(QFIvvE$#M`Hr!Jyh2auZPg|kZJw2R`l6-1C*1KO}z0;NMyDoOg#9>ft-5a&?w+1`K;|Hbw zlzcj?b&E{)PI|7~lJX+3Q&xD+-thBon>FY_1N`5P^Q1lq=t+E(Z6PZy&0J5c<;VK2 z-Ft0H$D`?_#hS9&L^>J8Hja1Q#*r9wD)P5RM7%1cPEyPxm&rU|EY&mnnOPym<^|=IF{HbtyL$^8)D;bz~oB8HjrOi0Y2? zwj#&PUK&O(nt6`p;6OFY=|AhN%EsqX`qxdh!#5MBDm$tac(!`Q-mLg7Qu7>2clyx& zn~$MrJyqa%Qfe+ci$WyfjVDpMbEm z>pjf=oSPVb>rG5Z=TGET&G#Pa+VDBvcx;tYso(S_W-}k|T-QXW@vUa~iXy`oGJN4k z7mjp=C&QQdEAGapUq8LqxB?D%QONBDSwn6wv$wo#mWc2dP=;atmfa$!P9}QAM=hY0P_O#Le_wJ zfq8*>ArKIVATV$4xLjbX6(5Axbyq@0c->(Me%&?@-T(-%JJ}$-AiSQ0*D;a+39r** z!|5a}kT><5Pp6bfz43V7W(uR?O+0m72IO4PE-Nt1?Yve0rUd&0`x*4AP_--UbddAFy$Ou@PT??U6zo+-q(Q%{@phK-vEH` z>vRM80{MC(-?5nl2z|$Wh7J2>YlF_!%}MQoNkyNp>t7SKbQ^nnS7G0=IRO05g5Lzd zFTgK?9|S+)0l!*T(G6c^ee64~0O$+o3t0p71@r~(1?u%gy^fIt2zs3!8(oPTg@y56GtcFia@P|hgEQsZKuZhl z#u*sz3dZYL0toLc;Z1_@g76~JL8KF+{0)Wxb3U>+*u_D2L3bf(pu3>Ept}$V2t**c z8|sBh&QInB%T)f4NEcXKXM{B*=UXR(~Nuxl;f1hIvfB z96X&=9tg+F%<)4ND0fws2Gaxpa%Vwq3LqCC7eNhzns6oJH5NF3iFmCFfVqIVkTt+u zz+Avw2m}Nos6@O>f%Tb)*K}7xn26ULmf+WI1Jn&D5wAJfz`DS?W30m44Y#wnfrUWmMa$Mtt*gAA$}huy8;5tJo+27j5z&6#_7|A=JsD%VYeBsA5bjU zJ+lASmAQRZd)B|v?9Yq9H;%T2`XE+allR8zW`{Q!aTo3}7U5!fQIh1UwNH9T9Gx~kGNT%q;1FpVo< z3lp|5LDsN^30s)3g$V)yfe2y?Q>|V-$ydMm*uvzxD$YJFQvh3-+{wlk zCTwByJelekNq}cl-5x`-+ev>;L|$T2B6FSLuW$9+_~Y>FGj=!o z>juOd5aP9F8;lo>cPZmFtt4Q`YjqhC%6l$VBU-V|X^dJseNK)U|wKe2pcdjFfTAK1OfsP1m?}>YHX=mVSHfT zvBMHF%sU>J;M;Wr<_!Szj%OQ~7ns)*^SV|NAm$x+84}F<(7hZUzvL@v@%(~S9=q*W zquK`MU4eOBD*)!5#k>h%USM7XI|z2dgLxlRvDqT)LteK$5HAofL=A`+h!=<#0s(;t z0`VT#ODDyG55VgRE*sEp0BF~lYk*yVT~DxU+em;|*Xb}MP*=zvpVf|^ zZ&RnGfhb)LW2ag66i{~s)U|B@NO#fi(Y)^7MKWFxUc@?xb;3k=bxz|sAGvtl@*un* zybv`IUJzaoUI+vPA_&6E=9qlWPbOZ+SqT~8bw(xlblE_710cN4T!Zj}@Olzn+eQK; zymp5Tl{g49EUdS2IjqDxZ2#viO@4@7sk;rOl0XSY;c8B7(i|4!~$t(%RTltAZX-s=6k_0FrI84XHgEH~DJ vLL2f>Gamd(uk%1mF^xs6R?KLtEIz1)K#hw|zB${}&X4~CvK&3HAddk6z!wM! literal 3906 zcmV-I554doiwFpt8rfbYXIIEpTCHWiDfHZE#_3X>V?0Z*FF3X8`S;Yg6LN z7KXq3E3#8lTdgx%+%KLpXBQ%aC<93FdfY{Y(1A!uFbOwj&woF;f#DJ$h?z0-ewk6y zy_)WP~pgD{daBLBl<6Nk8Kg!_<)nUJyz0-oc0B=X}jM=1)Vl_DfeHSGn`+S~nei zQH`?tCAMoe7zsT-N>!p;R5a@fCrg?li>hskqC=CPoIlQf<$jMgKmGC9Di>Z-uF=;t zSvnHgF;Pg9{%l`XwXsNEi&L~&z-aw{_9xRz(|&QRG+@<<3Chu(?8244Y7Gm`_BGjg zVe-8ejWr1;E39VSpV!9qyT*!rt1q(BB#NVs{>{|fsHYIszO6hfs$_ckhr_zsmWPbS zYJIh!54GynM3eArU(`IbvA59J!oW4;QE1t)ow8h~rO17$>1pV6bIy;6WVmJ1rAM05 zCtD4e++#UG8@redheBIf9l6-s&f0jV)`mh;Ywx(fZ6dVV+K!DXvbO3u+0G~@i?ZVO zSbRDVB#kH%E1qaVm(=@`Fgm1;s80u6U)AL9iQ22T8;8_?sc@Y7i%O*9(NrozolpnU z61>Q&?Ybs#okQx&7b>h5ds1bo$7<~Zrx{%nHFEfw+W&N{sP*PRt@jkUcih-N)XjOS z5!pXHT=gIukH*rWuyp2jg*$9F#FpLcih+KM-HNOVtJG7FRG~?Bt87A7*Q%x3R_NMu z>c+Ar%-4XhLD+Wjbg(K(#4LYYZddAy;&y|;ibBivf#ZmwncIacgLkvc3!?Z-?@hPbP2jVCcEeDWYPq7PcYp1B3|L0{+l4E; zjNHPBWHK4if81f{#(NIk_^m^?52?s{>aPkfWJk9!67o3O>Xv>t`Q8~()N|{$B1oNG zOR>>}>yS@a&5_DV>UKjf_cc+F$f}S$7D=YU=};&Y+G19;fB1WEre%GGF+|J>A!t=2 zy^12YUTw}!gJyx5HtE-EhAb-M3^%cAex2!6j+OKYU*N}#0euQmrYER#H-(mDjTj%& z)f`jHRBKd+jH!Kxm+b2f*OtX56`|APBXDrKmG)Y7d{N?$U{ji~);lZVIj+`#dKG#f0}F+tVe zuP}@BW(G6lyq7dz>?v$v)EY{4ecN65_H3KnWJpV|6=RRkl%F}{1$u#oGeL%d#yIn ziP=(%-Zm45&3x#1zI4Ihy3H9=>l}af>2g|gYs4u}dTWRO=4FK=l*93;rSj+bYwfvw zMVf01VpAcevO7w>{`mJ|jJtG5Y2AZwbz|h5nCRHyFop{)v9~w#|J2DvnPbHp`_@~Y zK7)B9z`Ve`NOq9y1Pk+OiZQW!1HF?vK)pb{5I3M+pkAO}2m}PeFVtJBX0n%zN7!oz zEdj*6cF*`NLO38_pOCLT-hjSsI^;O;KlM`CY4lC_5j z!0!g&*YX3P-+A;K1^Na0Me>8>CvfOjQbo_`*Q^8d3-k+d1NsH}1^R_RKp=cXza^&V z9sL?XOEA%|(KCLF5Dw_qKlE#iH_$K8?>h9W`-%VPS8vqR{h;vQr}SMbbJ;4ExU*O$ z7U`yLB=Q3OyBYu0odNuJp8v+cf5Cr|0U-kl7XKB@fWRC7ozwyU1^-$E&$V`?7xuN|}mkpJ2}zxH^8|APOngK z zqhD@#_Qa|R)4cln(!V;Y9A(w<{W49abF&HkhNhOpW4{32H3BVu zsf?dsq8u~ zyFp7E!kvZ4w_A**;&M4wxGP@sLtYh!x0%FGihLKN)_?w47x;d01ipIdMH62T_#*H{ z;JaDi8>yC319I^YxyTlZA@<_*sC3yv;ETZbO@VLhgT}p;Og@KD_oIip4O!E>_O79B zEPNb^Bij8b(eCG!6Pm^(&WDi;;lA@S9G1IG>+1RX3=`U~GNHxdA9F^F$3Nzb)*{;E zZ|?_>^fXm^q|H?8ksi%vNglNx^WCp7-)VC1T_?ME?AWil?u}acTZ5haX+y4d;+>01 zFVCbO#OKmIDJ}y$MVVu*1wZe$ScCR8!T;?zPvV1sp4dlO7P8jUjQPZze$2nMyU$I@ zXgC=+SyMV4OUA=k#__JpIAX(YN%~d~2{(nrS%P_DQ>o{xm6}Is@4IgY$_M59#guQ= z4xzP<_8wb$CV~F%vukn9-k5pZ;N3Fzu6)S7E3<{Bn^%}xrgqIx5<4*WuN)XDyVUO< zo>0bP)mJE9Htx4-ZdAz>iPn#C){;E&o7%*L#@$ zIX5x-)|(g;FFWL3$@Uvnb@UvqKeY;p#BX{N)2R=)uglSBbf+hLg>k|cPWZx>E^O%v z&Iw<}Tio?;zxnZA(+b$&Md55O$QsV}!r5Lp+Y15#f$-sMuS&ICspgnmsg|pHINfU! zvjpsDuSvIfudy2r_446RugPHJL@%7^<$9u*?Ib=P=ViBOK2Je8&?}Tb&-9h^L?o4X zj)$Y=JTG*@IM8d&fnK&J;54s!%o_pb1?EM%gLEf2nAdP^UB5QRyweK6yuiGWHDF#~ zUSM7b1O&nl%$qqa=4$1V2f{lER{}LkarE_wVeTwcOLRa0eJy=k?bJZ2@d47Ud*n4 zvN`5;Q~~G(=!LWa^aAt(^gW1Nt(Ra;@<7-UITTgew69 zzLRe8Uc)xPULRoZWUzsHfqGq0uk9p0f?m7DdQW7>L1DbN%yTg!KlFvr@IwAJRFizW zegVe2hVj~-0Kz*@c;g_vAiT(Qkm&^I{03csSr4Z-Sj9niL3bf(pu3>Ept}$V2!tAa@FK_ok&Bhw(Z+ed>wElV`g23;|An0D}M{8$vd; zYXTfiL?X#({0)#2P!+%g5)oivV!ikL@lmQ|@;Q3v3N)bvv6FNZSw0f5YjYmw<1bZ1dG&rpk=Q(rxKV>58F;dN@?X z%jSRGL>X@&M`DY_7EvpfHqE>zv8jhbp58!iRlxElEN_CWVR;jlH(_}b1Ofu#(;LXo zvgL2yUOsMyE5UpDxY;e91Fcf z#8~P!#ogX(_tB@(*JmttTL0d0!wIn53GW>T+(nXuBq!i-x2ae3Mt7|mz`MY^5H{dl z;9cNd2m}PeFT8t}Va`4BT{B<_AmBCo#d{9jfOvgEyyj?w@q+QLWW0u#_zZc?CIdow zFU4|5&9_;VQ7Y%p$?***@^w(&4JfbS1;D&`^Efask{u*F0mi%>YYFP+nD?~?FfTAK zgbkP%m=~BA0s(>W1M_Axm0F=(Vmx5pX}}UN%scHD?>Te>=Jf&dPDdM<7ns)-^G>|P zN6b5IG9Z|@(YqdD;qs~`KlR#?dbthEy9V=4ya1RN?->W?MY4lrC%~Aup$N?u z*&Omt$^-EN@j}#qc!79f z1=w{3yOxjmh;{7-0|IsV^yx+A^!YwRlYh z3&M+B2f0p=2(QMf9P8mM-br~7UJzc08VD~4F9Fv4s1 ziuV|@f$;i3cs`ZoEpGtpon^gyc1gOW2u;Q@anM~s(#5Rbm8LNe-DOA9k5Zo}rK<99rik@>e9yFQ z7&j<^#!8&o`gik_S4YzgiexG`=7U1(@=(zq{8GQ$AcmMmLZ%fX+bRkTMHi@PQRj#A QruKgPA8#1xDsOoK0Ba`Q5&!@I diff --git a/schema.py b/schema.py index db77144..5a1340e 100644 --- a/schema.py +++ b/schema.py @@ -274,7 +274,9 @@ field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARM "silences), bypass (hold: hand a device to an operator - its MIDI links out, its Companion connections off).", "Keys", advanced=True, options=["master", "service", "network", "ready", "autofix", "bypass"]) field("tiles[].bypass", "dict", None, "A bypass key: peers (MIDI peer or session names to take out, e.g. Lighting), " - "hosts (the Macs whose sessions to look in; \"\" is this Mac) and conns (Companion connections to switch off).", + "hosts (the Macs whose sessions to look in; \"\" is this Mac) and conns (Companion connections to switch off); or " + "fixes (fix ids it holds off: their checks stay green and nothing runs them, e.g. ableset), with on_text / off_text " + "for the key's face.", "Keys", advanced=True) field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys") field("tiles[].checks", "list", [], "What the key watches.", "Keys") @@ -350,6 +352,8 @@ field("restart_page.keys[].id", "id", None, "", "Keys", advanced=True, required= field("restart_page.keys[].label", "str", None, "", "Keys", required=True, apply="pages") field("restart_page.keys[].pos", "list", None, "[row, column].", "Keys", required=True, apply="pages") field("restart_page.keys[].fix", "str", None, "What holding it runs.", "Keys", required=True) +field("restart_page.keys[].toggle", "str", None, "A switch kept on this page: the bypass key (no position of its own) " + "it shows and a hold flips, e.g. ableset.", "Keys", apply="pages") field("restart_page.keys[].disabled", "bool", False, "Switched off: the key stays on the page (OFF) but a hold does " "nothing.", "Keys", apply="pages") field("restart_page.keys[].watch", "list", [], "Keep flashing HOLD TO FIX while these fixes' checks or " @@ -565,7 +569,7 @@ GLOSSARY = { "ON FOH BACKUP", "BUSY", "TAKING MIDI OUT", "PUTTING MIDI BACK", "TAKING", "PUTTING", "MIDI OUT", "MIDI BACK", ], "a switch key (AUTO-FIX, or one like LIGHTS that hands a device to an operator)": [ - "OPERATOR", "MIDI OFF", "OFF IN SETTINGS", "SETTINGS", "TIL", "PAUSED", + "OPERATOR", "MIDI OFF", "AUTO-OPEN", "OFF IN SETTINGS", "SETTINGS", "TIL", "PAUSED", ], } @@ -732,7 +736,9 @@ def validate(cfg, live=None): fixes_of = {c.get("fix") for c in t.get("checks", []) if isinstance(c.get("fix"), str)} | {"conn:" + c["conn"] for c in t.get("checks", []) if c.get("conn")} if fixes_of & on_restart: continue - if "pos" not in t and "members" not in t and t.get("id") not in in_group and t.get("kind") != "master": + toggled = {k.get("toggle") for k in (cfg.get("restart_page") or {}).get("keys", [])} + if "pos" not in t and "members" not in t and t.get("id") not in in_group and t.get("kind") != "master" \ + and t.get("id") not in toggled: warnings.append("key %s has no place on the deck and is in no group: nobody will see it" % t.get("id")) if kinds["master"] != 1: errors.append("there must be exactly one ALL key (kind master); found %d" % kinds["master"]) @@ -745,8 +751,11 @@ def validate(cfg, live=None): for t in cfg["tiles"]: if t.get("kind") == "bypass": b = t.get("bypass") if isinstance(t.get("bypass"), dict) else {} - if not b.get("peers"): - errors.append("key %s: a bypass key needs bypass.peers (the MIDI peers it takes out)" % t.get("id")) + if not b.get("peers") and not b.get("fixes"): + errors.append("key %s: a bypass key needs bypass.peers (the MIDI peers it takes out) or bypass.fixes" % t.get("id")) + for f in b.get("fixes") or []: + if f not in cfg.get("fixes", {}): + errors.append("key %s: bypass fix %s is not a fix in the settings" % (t.get("id"), f)) for h in b.get("hosts") or []: if h and h not in cfg.get("hosts", {}): errors.append("key %s: bypass host %s is not a computer in the settings" % (t.get("id"), h)) @@ -857,6 +866,8 @@ def validate(cfg, live=None): errors.append("%s: a key on the Status page has that id" % where) if k.get("fix") not in fixes: errors.append("%s: unknown fix %s" % (where, k.get("fix"))) + if k.get("toggle") and not any(t.get("id") == k["toggle"] and t.get("kind") == "bypass" for t in cfg.get("tiles", [])): + errors.append("%s: toggle %s is not a bypass key" % (where, k["toggle"])) for w in k.get("watch", []): if not w.startswith("conn:") and w not in fixes: errors.append("%s: unknown fix %s in its watch list" % (where, w)) diff --git a/status_panel.py b/status_panel.py index 955b2c6..285e7f9 100644 --- a/status_panel.py +++ b/status_panel.py @@ -592,6 +592,8 @@ def bypass_apply(cfg, tile, on, entry, comp, names_ips, lock=None): taken out ({host, session, peer, address}), so taking it back puts exactly those back; the MIDI checks naming it are reconnected too. Its Companion connections are switched off, then on again.""" b = tile.get("bypass") or {} + if not b.get("peers") and not b.get("conns"): + return # a key that only holds off fixes: nothing to hand over names, ips = set(b.get("peers") or []), set(names_ips[1]) if on else set() hosts = b.get("hosts") or [""] + sorted({c["host"] for t in cfg["tiles"] for c in t.get("checks", []) if c.get("type") == "midi_peer" and c.get("peer") in names and c.get("host")}) @@ -1410,7 +1412,7 @@ class Engine: def evaluate(self, snap, now, service_on): ctx = {"now": now, "service": service_on, "any_recording": False, "dante_all": {}, "ready_due": in_windows(self.ready_cfg.get("due", []), now), "ready_done": self.ready_done(now), - "bypassed": self.bypass_names()[0], "autofix_windows": (self.cfg.get("auto_fix") or {}).get("windows", [])} + "bypassed": self.bypass_names()[0], "bypassed_fixes": self.bypass_fixes(), "autofix_windows": (self.cfg.get("auto_fix") or {}).get("windows", [])} for t in all_tiles(self.cfg): for chk in t.get("checks", []): if chk["type"] == "dante": @@ -1426,6 +1428,8 @@ class Engine: for i, chk in enumerate(t.get("checks", [])): try: results = CHECKS[chk["type"]](chk, snap, ctx) + if isinstance(chk.get("fix"), str) and chk["fix"] in ctx["bypassed_fixes"]: + results = [(sub, Result(OK)) for sub, _ in results] # switched off by its bypass key except Exception as e: # a bad check must not take the panel down log.exception("check %s/%s failed", t["id"], chk["type"]) results = [("error", Result(WARN, "CHECK BUG"))] @@ -1748,6 +1752,10 @@ class Engine: ips.add(str(ip)) return names, ips + def bypass_fixes(self): + """Fixes a bypass key holds off (an ABLESET key: AbleSet not watched nor opened while it is on).""" + return {f for tid in self.bypass for f in (((self.tiles.get(tid) or {}).get("bypass") or {}).get("fixes") or [])} + def toggle_bypass(self, tile_id, comp, now): """A hold on a bypass key: hand it to an operator (take its MIDI links out, switch its Companion connections off), or take it back (put everything back, as it was).""" @@ -1763,8 +1771,13 @@ class Engine: entry = self.bypass[tile_id] = {"since": now, "dropped": []} on = True self.state.set("bypass", self.bypass) - log.info("BYPASS %s: %s", tile["label"], "handed to an operator" if on else "back to the panel") - self.note(tile_id, "TAKING\nMIDI OUT" if on else "PUTTING\nMIDI BACK", "fixing", now, 6) + b = tile.get("bypass") or {} + if b.get("peers") or b.get("conns"): + log.info("BYPASS %s: %s", tile["label"], "handed to an operator" if on else "back to the panel") + self.note(tile_id, "TAKING\nMIDI OUT" if on else "PUTTING\nMIDI BACK", "fixing", now, 6) + else: # a switch that only holds off fixes: its face says it all + log.info("BYPASS %s: %s", tile["label"], "switched off (%s not watched or run)" % ", ".join(b.get("fixes") or []) + if on else "back on") self.bypass_last = now self.start_bypass_job(tile_id, on, entry, comp) @@ -2409,7 +2422,9 @@ def render(engine, service_on): if kind == "bypass": # Handed to an operator: steady blue, says so; otherwise the panel runs it. on = tid in engine.bypass - out[tid] = colored("%s\n%s" % (tile["label"], "OPERATOR\nMIDI OFF" if on else "AUTO"), "service" if on else "ok") + b = tile.get("bypass") or {} + face = (b.get("on_text") or "OPERATOR\nMIDI OFF") if on else (b.get("off_text") or "AUTO") + out[tid] = colored("%s\n%s" % (tile["label"], face), "service" if on else "ok") continue if kind == "master": # Idle faults are expected outside service; they do not stop "ALL OK". @@ -2461,6 +2476,9 @@ def render(engine, service_on): else: text = tile["label"] + "\n" + (infos[0] if infos else "OK") out[tid] = colored(text + failover_line(engine, [tid]), level) + for kid, key in engine.restart_keys.items(): # a Restart page key that is a switch shows the switch + if key.get("toggle") in out: + out[kid] = out[key["toggle"]] return out diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index f2057b0..e6558fb 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -2634,6 +2634,26 @@ class TestSwitches(unittest.TestCase): self.assertIsNone(res) self.assertIn("calls in", why) + def test_ableset_key_holds_off_its_fix(self): + """ABLESET (2026-10-25, the user had an AbleSet bug): held, AbleSet is neither watched nor opened.""" + engine = sp.Engine(CONFIG) + snap = sp.Snapshot() + snap.remote["tracks\0pgrep -xq AbleSet"] = False + tiles, _ = run_steps(engine, snap, SUNDAY, 4) + msgs = lambda: [res.msg for _, res, _ in engine.items.get("apps", [])] + self.assertIn("ABLESET NOT RUNNING", msgs()) + self.assertEqual(tiles["ableset"]["x"], "ABLESET\nAUTO-OPEN\nON") + self.assertIn("ableset", [w for _, k, w, _ in engine.fix_plan("apps", snap)]) + engine.bypass["ableset"] = {"since": SUNDAY, "dropped": []} + tiles, _ = run_steps(engine, snap, SUNDAY + 10, 4) + self.assertNotIn("ABLESET NOT RUNNING", msgs()) + self.assertEqual(tiles["ableset"]["x"], "ABLESET\nAUTO-OPEN\nOFF") + self.assertEqual(tiles["rs_ableset"]["x"], "ABLESET\nAUTO-OPEN\nOFF") # its key on the Restart page + key = make_page.restart_key(next(k for k in CONFIG["restart_page"]["keys"] if k["id"] == "rs_ableset"), CONFIG) + self.assertIn("restart ableset", json.dumps(key)) # the hold flips the switch + self.assertNotIn("ableset", [w for _, k, w, _ in engine.fix_plan("apps", snap)]) + sp.bypass_apply(CONFIG, engine.tiles["ableset"], True, engine.bypass["ableset"], None, (set(), set())) # nothing to hand over + def test_a_bypass_key_needs_its_peers(self): import schema bad = json.loads(json.dumps(self.cfg))