- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
190 lines
8.3 KiB
Python
190 lines
8.3 KiB
Python
"""Patch (2026-10-12, user request): batch confirmation of required sections and batch run + auto-approval of
|
|
gates G1..G5. Approvals are recorded under the name typed by the user, note "일괄 실행 자동 승인"; only PASS
|
|
gates are approved and the batch stops at the first FAIL / error / blocker."""
|
|
from pathlib import Path
|
|
|
|
APP = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def patch(rel, reps):
|
|
p = APP / rel
|
|
s = p.read_text(encoding="utf-8")
|
|
for a, b in reps:
|
|
assert s.count(a) == 1, (rel, a[:70])
|
|
s = s.replace(a, b)
|
|
p.write_text(s, encoding="utf-8")
|
|
|
|
|
|
patch("web/app.py", [
|
|
("""ACTIVE = {} # job id -> thread of this server process""",
|
|
"""ACTIVE = {} # job id -> thread of this server process
|
|
BATCH = {} # pier name -> batch thread of this server process
|
|
AUTO_NOTE = "일괄 실행 자동 승인\""""),
|
|
(""" return redirect(request.form.get("next") or url_for("pier", name=name))
|
|
|
|
|
|
# ---------------- gates""",
|
|
""" return redirect(request.form.get("next") or url_for("pier", name=name))
|
|
|
|
|
|
def unconfirmed(p):
|
|
\"\"\"Required sections (all gates) that exist and are not confirmed, without duplicates.\"\"\"
|
|
out = []
|
|
for g in gate_ids():
|
|
for r in p.gate_requirements(g):
|
|
if r["exists"] and not r["confirmed"] and (r["file"], r["key"]) not in out:
|
|
out.append((r["file"], r["key"]))
|
|
return out
|
|
|
|
|
|
@app.post("/pier/<name>/confirm_all")
|
|
def confirm_all(name):
|
|
p = pier_or_404(name)
|
|
by = request.form.get("by", "").strip()
|
|
if not by:
|
|
flash("확정자 이름을 입력하세요.", "fail")
|
|
return redirect(url_for("pier", name=name))
|
|
need = unconfirmed(p)
|
|
for f, k in need:
|
|
p.confirm(f, k, by)
|
|
flash(f"필요한 묶음 {len(need)}개를 일괄 확정했습니다 ({by}).", "ok")
|
|
return redirect(url_for("pier", name=name))
|
|
|
|
|
|
# ---------------- gates"""),
|
|
("""@app.post("/pier/<name>/gate/<gate>/run")
|
|
def gate_run(name, gate):
|
|
p = pier_or_404(name)
|
|
if gate not in gate_ids():
|
|
abort(404)""",
|
|
"""def batch_state(p):
|
|
\"\"\"State of the batch run of this pier (실행 중 only while its thread lives in this server process).\"\"\"
|
|
b = store.rjson(p.run_dir / "batch.json")
|
|
if b and b["state"] == "실행 중":
|
|
t = BATCH.get(p.name)
|
|
if b.get("pid") != os.getpid() or not (t and t.is_alive()):
|
|
b = dict(b, state="중단됨")
|
|
return b
|
|
|
|
|
|
def _batch(p, by, gates_todo):
|
|
bp = p.run_dir / "batch.json"
|
|
log = []
|
|
|
|
def put(state):
|
|
store.wjson(bp, {"state": state, "by": by, "pid": os.getpid(), "at": store.now(), "log": log})
|
|
put("실행 중")
|
|
for g in gates_todo:
|
|
b = gate_blockers(p, g)
|
|
if b:
|
|
log.append(f"{g} 실행 불가: " + ", ".join(b))
|
|
return put("중단")
|
|
jid = uuid.uuid4().hex[:12]
|
|
store.wjson(p.run_dir / f"{g}_job.json", {"id": jid, "pid": os.getpid(), "state": "대기", "at": store.now(), "log": []})
|
|
ACTIVE[jid] = threading.current_thread()
|
|
log.append(f"{g} 실행 시작")
|
|
put("실행 중")
|
|
_gate_job(p, g, jid)
|
|
st = p.gate_status(g)
|
|
rep, job = st["report"] or {}, st["job"] or {}
|
|
if job.get("state") != "완료" or rep.get("job_id") != jid or rep.get("verdict") != "PASS":
|
|
log.append(f"{g} {rep.get('verdict') or job.get('state')} — 승인하지 않고 멈춤 (결과를 확인하세요)")
|
|
return put("중단")
|
|
p.set_approval(g, "승인", by, AUTO_NOTE)
|
|
s = rep["summary"]
|
|
log.append(f"{g} PASS {s['passed']}/{s['total']} (계획 FAIL {len(rep['planned_fails'])}) → 승인")
|
|
put("실행 중")
|
|
put("완료")
|
|
|
|
|
|
def anything_running(p):
|
|
if (batch_state(p) or {}).get("state") == "실행 중":
|
|
return True
|
|
return any(job_state(p.gate_status(g)["job"]) in ("대기", "실행 중") for g in gate_ids())
|
|
|
|
|
|
@app.post("/pier/<name>/gates/run_all")
|
|
def gates_run_all(name):
|
|
p = pier_or_404(name)
|
|
by = request.form.get("by", "").strip()
|
|
if not by:
|
|
flash("판정자 이름을 입력하세요.", "fail")
|
|
return redirect(url_for("gates", name=name))
|
|
if anything_running(p):
|
|
flash("이미 실행 중인 작업이 있습니다.", "fail")
|
|
return redirect(url_for("gates", name=name))
|
|
todo = [g for g in gate_ids() if not p.gate_status(g)["approved"]]
|
|
if not todo:
|
|
flash("모든 게이트가 이미 승인되어 있습니다.", "ok")
|
|
return redirect(url_for("gates", name=name))
|
|
b = gate_blockers(p, todo[0])
|
|
if b:
|
|
flash(f"{todo[0]} 실행 불가: " + ", ".join(b), "fail")
|
|
return redirect(url_for("gates", name=name))
|
|
store.wjson(p.run_dir / "batch.json", {"state": "실행 중", "by": by, "pid": os.getpid(), "at": store.now(),
|
|
"log": ["일괄 실행 대기"]})
|
|
t = threading.Thread(target=_batch, args=(p, by, todo), daemon=True)
|
|
BATCH[p.name] = t
|
|
t.start()
|
|
flash(f"{todo[0]}~{todo[-1]} 연속 실행을 시작했습니다. PASS인 게이트만 '{by}' 이름으로 자동 승인합니다.", "ok")
|
|
return redirect(url_for("gates", name=name))
|
|
|
|
|
|
@app.post("/pier/<name>/gate/<gate>/run")
|
|
def gate_run(name, gate):
|
|
p = pier_or_404(name)
|
|
if gate not in gate_ids():
|
|
abort(404)
|
|
if (batch_state(p) or {}).get("state") == "실행 중":
|
|
flash("일괄 실행 중에는 개별 실행을 할 수 없습니다.", "fail")
|
|
return redirect(url_for("gates", name=name))"""),
|
|
(""" for r in rows:
|
|
r["state"] = job_state(r["job"])
|
|
running = any(r["state"] in ("대기", "실행 중") for r in rows)""",
|
|
""" for r in rows:
|
|
r["state"] = job_state(r["job"])
|
|
batch = batch_state(p)
|
|
running = any(r["state"] in ("대기", "실행 중") for r in rows) or (batch or {}).get("state") == "실행 중\""""),
|
|
])
|
|
|
|
patch("web/app.py", [
|
|
(""" return render_template("gates.html", p=p, rows=rows, running=running)""",
|
|
""" return render_template("gates.html", p=p, rows=rows, running=running, batch=batch,
|
|
todo=[g for g in gate_ids() if not p.gate_status(g)["approved"]])"""),
|
|
(""" return render_template("pier.html", p=p, sections=secs, gates=gates,""",
|
|
""" return render_template("pier.html", p=p, sections=secs, gates=gates, need=unconfirmed(p),"""),
|
|
])
|
|
|
|
patch("web/templates/pier.html", [
|
|
("""<h2>3. 게이트</h2>""",
|
|
"""<form method="post" action="{{ url_for('confirm_all', name=p.name) }}"
|
|
onsubmit="return confirm('게이트에 필요한 미확정 묶음 {{ need|length }}개를 모두 확정합니다. 값을 도면·판독서와 대조하셨습니까?');">
|
|
{% if need %}확정자 <input type="text" name="by" required>
|
|
<button>필요한 묶음 일괄 확정 ({{ need|length }}개)</button>
|
|
<span class="muted">게이트 G1~G5에 필요한 묶음 중 미확정인 것만 확정합니다.</span>
|
|
{% else %}<span class="ok">게이트에 필요한 묶음이 모두 확정되어 있습니다.</span>{% endif %}
|
|
</form>
|
|
|
|
<h2>3. 게이트</h2>"""),
|
|
])
|
|
|
|
patch("web/templates/gates.html", [
|
|
("""<h2>검토보고서 (초안)</h2>""",
|
|
"""<h2>연속 실행·자동 승인</h2>
|
|
{% if batch %}<p>최근 일괄 실행: <b class="{{ 'ok' if batch.state == '완료' else ('warn' if batch.state in ('실행 중', '중단됨') else 'fail') }}">{{ batch.state }}</b>
|
|
· 판정자 {{ batch.by }} · {{ batch.at }}</p>
|
|
<ul>{% for m in batch.log %}<li>{{ m }}</li>{% endfor %}</ul>{% endif %}
|
|
{% if todo %}
|
|
<form method="post" action="{{ url_for('gates_run_all', name=p.name) }}"
|
|
onsubmit="return confirm('{{ todo[0] }}~{{ todo[-1] }}를 차례로 실행하고 PASS인 게이트만 입력한 이름으로 자동 승인합니다. 진행할까요?');">
|
|
판정자 <input type="text" name="by" required>
|
|
<button {% if running %}disabled{% endif %}>{{ todo[0] }}~{{ todo[-1] }} 연속 실행·자동 승인</button>
|
|
</form>
|
|
<p class="muted">PASS인 게이트만 승인합니다. FAIL·오류·실행 조건 미충족이면 그 게이트에서 멈추고 승인하지 않습니다.
|
|
승인 기록의 의견란에 '일괄 실행 자동 승인'이 남습니다. 결과는 아래 게이트별 표에서 확인하세요.</p>
|
|
{% else %}<p class="ok">모든 게이트가 승인되어 있습니다.</p>{% endif %}
|
|
|
|
<h2>검토보고서 (초안)</h2>"""),
|
|
])
|
|
print("patched")
|