@@ -910,3 +910,189 @@ These backends target legitimate use cases — accessibility software,
910910GUI testing of games that lock out user-mode input, controlling a
911911remote game-running machine from a headless setup — and aren't a
912912generic anti-cheat bypass.
913+
914+
915+ Per-action profiler
916+ ===================
917+
918+ Records wall-clock duration for every ``AC_* `` action so you can answer
919+ "which step is dominating this script's runtime?" without external
920+ tooling. Profiling is opt-in — when disabled, the executor wrapper has
921+ zero overhead::
922+
923+ import je_auto_control as ac
924+ ac.default_profiler.enable()
925+ ac.execute_action([["AC_locate_image_center", {"image": "btn.png"}],
926+ ["AC_click_mouse"]])
927+ for row in ac.default_profiler.hot_spots(limit=5):
928+ print(row.name, row.calls, row.average_seconds)
929+
930+ Action-JSON commands::
931+
932+ [["AC_profiler_enable"]]
933+ [["AC_profiler_stats", {"limit": 10}]]
934+ [["AC_profiler_hot_spots", {"limit": 5}]]
935+ [["AC_profiler_reset"]]
936+ [["AC_profiler_disable"]]
937+
938+ GUI: **Profiler ** tab — live hot-spot table (calls / total / avg / min /
939+ max / share) refreshed every second. Toggle recording, reset stats, or
940+ export the snapshot through the headless API.
941+
942+
943+ Run history timeline + failure thumbnails
944+ =========================================
945+
946+ The Run History tab gains a Gantt-style strip beneath the filter row:
947+ each scheduler / trigger / hotkey / webhook / email fire is rendered as
948+ a coloured bar on a horizontal time axis (green = ok, red = error,
949+ amber = still running). Selecting a bar syncs the table row, and a
950+ right-hand preview panel surfaces the failure screenshot already
951+ captured by the artifact manager.
952+
953+ Headless callers query the same data through the existing run history
954+ store::
955+
956+ import je_auto_control as ac
957+ for row in ac.default_history_store.list_runs(limit=20):
958+ print(row.id, row.status, row.duration_seconds, row.artifact_path)
959+
960+ No new commands — the store API is unchanged. The GUI is purely a
961+ thin visualization wrapper over the existing `runs ` table.
962+
963+
964+ Encrypted secret manager
965+ ========================
966+
967+ Action scripts that need API tokens, IMAP passwords, etc. should never
968+ embed plaintext. The new vault stores Fernet-encrypted entries under
969+ ``~/.je_auto_control/secrets/vault.json ``; a passphrase derives the
970+ key via PBKDF2-HMAC-SHA256 (600,000 iterations, 16-byte salt)::
971+
972+ import je_auto_control as ac
973+ ac.default_secret_manager.initialize("my-vault-passphrase")
974+ ac.default_secret_manager.set("github_token", "ghp_xxxxx")
975+ ac.default_secret_manager.lock()
976+
977+ # later — in the same process or a new run:
978+ ac.default_secret_manager.unlock("my-vault-passphrase")
979+
980+ Action-JSON commands::
981+
982+ [["AC_secret_init", {"passphrase": "..."}]]
983+ [["AC_secret_unlock", {"passphrase": "..."}]]
984+ [["AC_secret_set", {"name": "github_token", "value": "ghp_xxx"}]]
985+ [["AC_secret_list"]]
986+ [["AC_secret_remove", {"name": "github_token"}]]
987+ [["AC_secret_lock"]]
988+ [["AC_secret_status"]]
989+
990+ Action scripts reference vault entries through ``${secrets.NAME} ``
991+ placeholders. The interpolator routes the ``secrets. `` namespace to the
992+ vault rather than the regular variable scope, so plaintext values never
993+ land in the variable bag::
994+
995+ [["AC_shell_command",
996+ {"command": "curl -H \"Authorization: Bearer ${secrets.github_token}\" ..."}]]
997+
998+ GUI: **Secrets ** tab — initialize the vault, unlock it, add / remove
999+ entries, change passphrase. The vault file is created with mode 0o600
1000+ on POSIX systems; on Windows the default ACL already restricts
1001+ read access to the owning user.
1002+
1003+
1004+ Webhook (HTTP push) trigger
1005+ ===========================
1006+
1007+ A bundled :mod: `http.server ` dispatcher fires an action script when an
1008+ external service POSTs to a registered path. Configure path, allowed
1009+ methods, and an optional bearer token; the request method, path, query,
1010+ headers, raw body, and parsed JSON are seeded into the variable scope::
1011+
1012+ import je_auto_control as ac
1013+ ac.default_webhook_server.add(
1014+ path="/jobs/build", script_path="hooks/on_build.json",
1015+ methods=["POST"], token="topsecret",
1016+ )
1017+ host, port = ac.default_webhook_server.start("127.0.0.1", 0)
1018+ print("listening on", host, port)
1019+
1020+ The bound script reads the request through ``${webhook.*} `` placeholders::
1021+
1022+ [
1023+ ["AC_set_var", {"name": "branch", "value": "${webhook.query.ref}"}],
1024+ ["AC_shell_command",
1025+ {"command": "echo received build for ${webhook.body}"}]
1026+ ]
1027+
1028+ Action-JSON commands::
1029+
1030+ [["AC_webhook_start", {"host": "127.0.0.1", "port": 8765}]]
1031+ [["AC_webhook_add", {"path": "/jobs", "script_path": "...",
1032+ "methods": ["POST"], "token": "..."}]]
1033+ [["AC_webhook_list"]]
1034+ [["AC_webhook_remove", {"webhook_id": "abcd1234"}]]
1035+ [["AC_webhook_status"]]
1036+ [["AC_webhook_stop"]]
1037+
1038+ Each fire is recorded in run history as ``trigger `` with source id
1039+ ``webhook:<id> `` so the dashboard surfaces webhook activity alongside
1040+ other triggers. The body is capped at 1 MiB and bearer-token comparison
1041+ uses :func: `hmac.compare_digest `. Bind to ``127.0.0.1 `` unless the
1042+ listener genuinely needs to be reachable from elsewhere on the network.
1043+
1044+ GUI: **Webhooks ** tab — start/stop the server, register paths, view the
1045+ fire counter and auth state per route.
1046+
1047+
1048+ IMAP email trigger
1049+ ==================
1050+
1051+ Poll-based watcher that logs into a mailbox on a configurable interval
1052+ and runs an action script once per matching message::
1053+
1054+ import je_auto_control as ac
1055+ ac.default_email_trigger_watcher.add(
1056+ host="imap.gmail.com", username="user@example.com",
1057+ password="app-specific-password",
1058+ script_path="hooks/on_alert.json",
1059+ mailbox="INBOX", search_criteria='UNSEEN FROM "alerts@..."',
1060+ poll_seconds=120, mark_seen=True,
1061+ )
1062+ ac.default_email_trigger_watcher.start()
1063+
1064+ The bound script sees the message metadata via ``${email.*} ``::
1065+
1066+ [
1067+ ["AC_if_var", {
1068+ "name": "email.subject", "op": "contains", "value": "CRITICAL",
1069+ "then": [["AC_hotkey", {"keys": ["ctrl", "alt", "p"]}]]
1070+ }]
1071+ ]
1072+
1073+ Variables seeded per fire: ``email.uid ``, ``email.from ``, ``email.to ``,
1074+ ``email.subject ``, ``email.message_id ``, ``email.date ``, ``email.body ``.
1075+
1076+ Action-JSON commands::
1077+
1078+ [["AC_email_trigger_add", {"host": "...", "username": "...",
1079+ "password": "${secrets.imap_pw}",
1080+ "script_path": "...",
1081+ "mailbox": "INBOX",
1082+ "search_criteria": "UNSEEN",
1083+ "poll_seconds": 120,
1084+ "mark_seen": true,
1085+ "use_ssl": true}]]
1086+ [["AC_email_trigger_start"]]
1087+ [["AC_email_trigger_poll_once"]]
1088+ [["AC_email_trigger_list"]]
1089+ [["AC_email_trigger_remove", {"trigger_id": "abcd1234"}]]
1090+ [["AC_email_trigger_stop"]]
1091+
1092+ The watcher tracks already-fired UIDs in process memory, and optionally
1093+ flags messages ``\\Seen `` so the same mail isn't replayed across
1094+ restarts. TLS is pinned at 1.2 minimum. Combine ``AC_email_trigger_add ``
1095+ with ``${secrets.NAME} `` so passwords never appear in the JSON.
1096+
1097+ GUI: **Email Triggers ** tab — register IMAP triggers, start/stop the
1098+ watcher, run a manual poll, inspect last error and fire counter.
0 commit comments