Move all three skills into skills/; add root README; fix paths

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Sonnet 5.5 committed 2026-10-16 17:04:38 +05:30
1 parent 312742cfef
commit d95e5d452d
84 files changed
+97 -31

No files matched your search

@@ -0,0 +1,171 @@
"""Drive the Run tab's 7-box checkpoint tracker.
kind: 'active' (box cp is now running) | 'done' (box cp finished) |
'stuck' (box cp overran its max time)."""
self.send_to_js("cpstate", {"kind": kind, "cp": int(cp)})
def _on_automation_line(self, line, level):
"""Called (from the worker thread) for every printed line."""
# Watchdog checkpoint state marker -> 7-box tracker in the Run tab
# (raw line still stored in the DB log below).
if "[CPSTATE]" in line:
try:
kind, cp = line.split("[CPSTATE]", 1)[1].split()
self._cpstate(kind, cp)
# Feed the parent-side stall watchdog. The automation runs in a
# CHILD process, so the in-process performer.WD_STATE the old
# watchdog read is ALWAYS empty here (it never fired — a stuck
# checkpoint never restarted). Track the active checkpoint + its
# start time from the STREAMED [CPSTATE] line instead.
if kind == "active":
self._wd_cp = int(cp)
self._wd_since = time.time()
except Exception:
pass
low = line.lower()
ltype = "info"
if "[error]" in low or "[fatal]" in low or "traceback" in low or "exception" in low:
ltype = "error"
elif "warning" in low or "[warn" in low:
ltype = "warn"
elif "processed successfully" in low or "[all done]" in low or "success" in low:
ltype = "success"
# The FULL raw technical line always goes to the per-batch DB log AND to
# the DB-independent file log (the file works even when the DB is down).
if self._batch_log is not None:
self._batch_log.log(ltype, line)
if self._run_log_file is not None:
try:
self._run_log_file.write(line + "\n")
except Exception:
pass
# The on-screen log gets the plain-English translation only; raw lines
# stay off the screen — except untranslated ERRORS, which are shown
# as-is (a failure must never be invisible to the user).
shown = friendly_log_line(line)
if shown is not None:
if shown[0] != self._last_friendly: # collapse repeated polling lines
self._log(shown[0], shown[1])
self._last_friendly = shown[0]
elif ltype == "error" and not _is_error_noise(low):
# An untranslated real error still shows (a failure must never be
# invisible) — but raw traceback/COM noise is filtered out.
self._log(line, "error")
self._last_friendly = None
# advance the progress bar / step list on known markers
for marker, step_idx, pct in STEP_MARKERS:
if marker in low:
if step_idx > self._cur_step:
self._cur_step = step_idx
if pct > self._max_pct:
self._max_pct = pct
self._step(STEPS[self._cur_step], self._max_pct)
break
def _resolve_input_path(self, p):
"""Drag-drop in QWebEngine often hides the real disk path, so we may only
get a bare filename. Resolve it against the known input folders so the
automation always receives a real, absolute path."""
if p and os.path.isabs(p) and os.path.exists(p):
return p
base = os.path.basename(p) if p else ""
for folder in (INPUT_DIR, _ROOT):
cand = os.path.join(folder, base) if base else ""
if cand and os.path.exists(cand):
return cand
return p # unresolved — caller will report a clear error
def _run_multi_subprocess(self, excel_path, emp):
"""Run performer.run_multi in a SEPARATE PROCESS (its own MAIN thread,
no Qt) and stream its stdout back through this run's log pipeline.
Why a subprocess: pywinauto/UIA reads are reliable on a process MAIN
thread but flaky on a Qt BACKGROUND thread (STA-without-a-message-pump
for cross-process UIA; forcing MTA dead-locks against the Qt main
thread's UIA object). The reference tool works because it is a separate
process with no Qt — this reproduces that. Every child stdout line is
re-printed here so it flows through _StreamToBridge → _on_automation_line
exactly as an in-process print did (friendly log, [CPSTATE] tracker,
per-batch DB log, file log). Returns the run_multi summary dict.
"""
import subprocess
import time as _time
proj_root = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
# The child writes ALL its output to THIS file and we tail it live. A
# --windowed PyInstaller child's stdout PIPE is block-buffered/unreliable
# (v1.1.5: nothing streamed back), but a line-buffered file always
# flushes per line and can be read while the process runs.
try:
os.makedirs(LOGS_DIR, exist_ok=True)
except Exception:
pass
child_log = os.path.join(
LOGS_DIR,
f"auto_child_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
f"_{self.batch_id or 0}.log")
try:
open(child_log, "w", encoding="utf-8").close() # start empty
except Exception:
pass
if getattr(sys, "frozen", False):
cmd = [sys.executable, "--run-automation"]
else:
cmd = [sys.executable, os.path.join(proj_root, "run_app.py"),
"--run-automation"]
cmd += ["--excel", str(excel_path),
"--batch", str(self.batch_id or ""),
"--emp", str(emp or ""),
"--log", child_log,
# Parent-death watchdog: the child exits itself the instant this
# GUI process dies (crash, taskkill, or a closeEvent we couldn't
# run) so automation can never keep driving Tebra orphaned.
"--parent-pid", str(os.getpid())]
env = dict(os.environ)
env["PYTHONUNBUFFERED"] = "1"
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) \
if sys.platform.startswith("win") else 0
print(f"[Automation] Launching automation in its own process "
f"(reliable UIA) — batch {self.batch_id} …")
try:
proc = subprocess.Popen(
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
cwd=proj_root, env=env, creationflags=creationflags)
except Exception as e:
print(f"[Automation] Could not launch automation process: {e}")
return {"total": 0, "ok": [],
"failed": [("Automation", f"launch failed: {e}")],
"stopped": False}
self._auto_proc = proc
summary = {}
got_summary = False
def _handle(line):
nonlocal summary, got_summary
if line.startswith("__WF_SUMMARY__"):
try:
summary = json.loads(line[len("__WF_SUMMARY__"):]) or {}
got_summary = True
except Exception:
pass
elif line.strip():
print(line) # → _StreamToBridge tee → _on_automation_line
# Tail the child log until the process exits, then drain the rest.
try:
with open(child_log, "r", encoding="utf-8", errors="replace") as f:
while True:
pos = f.tell()
line = f.readline()
if line and line.endswith("\n"):
_handle(line.rstrip("\r\n"))
elif line: # partial line (no newline yet)
if proc.poll() is not None:
_handle(line.rstrip("\r\n"))