From 5ca83f48c3a7bc71b42f77025098f9e05beca004 Mon Sep 17 00:00:00 2001 From: Spencer McGuire Date: Sun, 18 Oct 2026 20:25:29 -0600 Subject: [PATCH] scripts: gitea_issues.py opens one Gitea issue per open decision in PLAN.md Co-Authored-By: Claude Fable 5.1 --- scripts/gitea_issues.py | 362 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 scripts/gitea_issues.py diff --git a/scripts/gitea_issues.py b/scripts/gitea_issues.py new file mode 100644 index 0000000..3bc2372 --- /dev/null +++ b/scripts/gitea_issues.py @@ -0,0 +1,362 @@ +# ruff: noqa: E501 S310 +"""Create one Gitea issue per open decision in docs/PLAN.md. + +Usage (token from a file, never from the command line or chat): + python scripts/gitea_issues.py --token-file %USERPROFILE%\\.config\\gitea\\token [--dry-run] + +Idempotent: an issue whose title already exists in the repo is skipped. Labels are created +when missing. The token needs the issue read and write scope on the repository. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +BASE = "https://gt.pescoinc.biz/api/v1" +REPO = "spencerm/PacketStation" +PLAN = "https://gt.pescoinc.biz/spencerm/PacketStation/src/branch/main/docs/PLAN.md" + +LABELS = { + "decision: Seth": "e11d48", + "decision: Q.C.": "7c3aed", + "decision: Clerical": "0ea5e9", + "decision: IT": "f59e0b", + "phase 3": "64748b", + "phase 4": "64748b", + "phase 5": "64748b", + "phase 6": "64748b", + "phase 7": "64748b", + "phase 8": "64748b", + "phase 9": "64748b", + "production": "16a34a", +} + +# (plan number or tag, title, labels, body) +ISSUES: list[tuple[str, str, list[str], str]] = [ + ( + "Q1", + "Station timer: keep, cross-check against VISUAL labor tickets, or drop", + ["decision: Seth", "phase 5"], + "The old build has its own clock per station and an Hours vs target report. VISUAL already has labor tickets.\n\n" + "**Built as the default:** the station timer stays the clock of record and the VISUAL ticket hours are shown beside it in every hours table. " + '"Over target" means timer hours over the work-order target; ticket hours never decide the flag (plan question 38).\n\n' + "**To answer:** keep as built, keep the timer but flag on ticket hours, or drop the timer and report from tickets only.", + ), + ( + "Q4a", + "Should Q.C. accept and filing require the domain password even when a PIN is set?", + ["decision: Seth", "decision: Q.C.", "phase 6"], + "Every sign-off (submit, send to Q.C., accept, reject, route, file, ITP block signatures, hand uploads, rev-mismatch approvals) takes a PIN or the domain password as the electronic signature.\n\n" + "**Built as the default:** the PIN is enough everywhere, including Q.C. accept and filing.\n\n" + "**To answer:** keep PIN for all, or require the domain password for Q.C. accept and for filing.", + ), + ( + "Q4b", + "Tie registered shop devices to a device certificate later?", + ["decision: IT"], + "PIN sign-in works only on devices an Admin or shop Supervisor registered; the device gets a long-lived cookie.\n\n" + "**Built as the default:** device cookie.\n\n" + "**To answer:** whether IT wants an auto-enrolled device certificate checked by the proxy on the domain-joined tablets as a later upgrade, and when.", + ), + ( + "Q5", + "Which customer order a packet shows when a project has several open orders", + ["decision: Seth", "phase 3"], + "VISUAL often has several open customer orders on one project. The packet's customer, PO and dates come from one of them.\n\n" + "**Built as the default** (one function, `feed/orders.py`): a Clerical pin, else the base's own linked order, else the project order whose top job is the unit, else the project's first open order, else its newest. " + "The source is stored and shown on the project page, which lists every open order; Clerical can pin another with a reason.\n\n" + "**To answer:** keep, or give a better rule.", + ), + ( + "Q6", + "VISUAL_ENCRYPT=yes once vsql has a trusted certificate", + ["decision: IT", "production"], + "The VisualKit connection runs with Encrypt=no because vsql has no certificate the container trusts.\n\n" + "**To answer:** whether IT will issue vsql a certificate from PESCO-PSYLOCKE-CA (now SHA-256) so the production stack can run with VISUAL_ENCRYPT=yes, and when.", + ), + ( + "Q7", + "Policy tightenings against the old build (ten rules to keep or veto)", + ["decision: Seth"], + 'The rebuild follows the old screens where they were stricter than the old rules. Each can be vetoed. Table: docs/POLICY.md, section "Tightenings against the old build".\n\n' + "- [ ] Only the holder of a packet edits shop data (welds, BOM, heat tags, checklist, weld record, timer); a lead of another department cannot.\n" + "- [ ] Only the assigned welder starts or submits a packet; leads and supervisors still can.\n" + "- [ ] Rev-mismatch approval and moving back from VISUAL review need a Clerical Supervisor (no Clerical lead group in AD).\n" + "- [ ] Packets routed to Shipping can be closed and filed; only the Data Book route needs the VISUAL close first.\n" + "- [ ] The data book date is Clerical or Q.C. only.\n" + "- [ ] A change-order verification cannot be withdrawn once signed.\n" + "- [ ] The warehouse cannot release until every BOM line not n/a is pulled or has a short on record.\n" + "- [ ] A hand-made draft needs a drawing before it leaves Clerical.\n" + "- [ ] Attaching a drawing by hand needs a signature.\n" + "- [ ] Admin bypasses custody to fix data but follows the workflow order.\n\n" + "Tick the ones to keep; comment on any to change.", + ), + ( + "Q9a", + "Retention period for packets, data books and audit (SECURITY.md D6)", + ["decision: Seth", "decision: Q.C."], + "Nothing is deleted today. A retention period lets a later phase close D6 and M5.\n\n**To answer:** how long filed packets, data books, MTRs and the audit log must be kept, and whether anything may ever be purged.", + ), + ( + "Q9b", + "Customer-facing data book copy (SECURITY.md D7)", + ["decision: Seth", "decision: Q.C."], + "The data book carries internal history and the controlled-copy stamp.\n\n**To answer:** whether a customer copy with some sections left out is wanted, and which sections.", + ), + ( + "Q10", + "Same-rev drawing re-release on an in-shop packet: change order or silent swap", + ["decision: Seth", "phase 3"], + "M-Files sometimes re-releases a drawing at the same rev with a new file.\n\n**Built as the default:** a drawing-origin change order, never a silent swap.\n\n**To answer:** keep, or swap silently with an audit entry.", + ), + ( + "Q11", + "Cancelled (X) work orders in VISUAL: flag only, or remove drafts", + ["decision: Seth", "phase 3"], + "**Built as the default:** a cancelled work order is flagged on the packet like a gone one; drafts are not removed.\n\n**To answer:** keep, or retire untouched drafts whose work orders were cancelled.", + ), + ( + "Q12", + "Partspecs, title or folded-operation target changes on an in-shop packet", + ["decision: Seth", "phase 3"], + "**Built as the default:** no change order for these, as the old build; the new values are stored and shown.\n\n**To answer:** keep, or open a change order for some of them.", + ), + ( + "Q13", + "Work-order-gone confirmation after 2 runs and the 10 percent suspect-batch guard", + ["decision: Seth", "phase 3"], + "A work order missing from VISUAL is flagged after two consecutive feed runs. A batch that lost more than 10 percent (and more than 5) of tracked work orders is applied without the gone step and marked degraded.\n\n**To answer:** keep the two numbers or change them.", + ), + ( + "Q14", + "Pause scope on the Feed settings tab", + ["decision: Seth", "phase 3"], + "**Built as the default:** pause stops the feed, the drawing job and auto-release together; run now still works.\n\n**To answer:** keep, or pause only the feed.", + ), + ( + "Q17", + "Who may set the data book date (was anyone, even on a filed packet)", + ["decision: Seth", "decision: Clerical"], + "**Built as the default:** Clerical or Q.C. only; still allowed on a filed packet.\n\n**To answer:** keep or widen.", + ), + ( + "Q18", + "Undoing signatures: change-order verifications and ITP line signatures", + ["decision: Seth", "decision: Q.C."], + "**Built as the default:** change-order verifications cannot be withdrawn; an ITP line signature can be undone by the same inspector or an Admin before the block is signed; a block signature by the signer or an Admin while the ITP is open.\n\n**To answer:** keep or change.", + ), + ( + "Q19", + "Warehouse release gate enforced on the server", + ["decision: Seth", "phase 4"], + "Two rules: every line not N/A is pulled or on an open short, and every pulled line that issued pieces has a heat unless N/A or a sub-assembly. The old build checked both on the screen only.\n\n**Built as the default:** both on the server, so a release cannot bypass them.\n\n**To answer:** keep or relax.", + ), + ( + "Q20", + "Hold and short reasons as closed lists", + ["decision: Seth", "phase 4"], + "**Built as the default:** closed sets (hold: Waiting on material, Waiting on purchasing / PO, Question for engineering or clerical, Drawing / BOM problem, Other with a note; short: Not in stock, On order, Back-ordered, Damaged / rejected, Wrong part received, Needs cutting / prep, Other).\n\n**To answer:** keep, add reasons, or allow free text.", + ), + ( + "Q21", + "Who may receive a short on a packet the warehouse no longer holds", + ["decision: Seth", "phase 4"], + "**Built as the default:** Warehouse keeps the right at any status but filed; the packet's holder (department lead or supervisor, Q.C., Clerical) can receive too.\n\n**To answer:** keep or narrow.", + ), + ( + "Q22", + "Upload kinds, size limits and virus scanning", + ["decision: Seth", "decision: IT", "phase 4"], + "**Built as the default:** MTR is PDF, PNG or JPG up to 25 MB; hand BOM is XLSX or CSV up to 5 MB; ClamAV is optional and off unless CLAMAV_HOST is set, and when set an unreachable scanner refuses uploads.\n\n**To answer:** keep the limits; whether IT wants ClamAV on in production.", + ), + ( + "Q23", + "Assets volume quota alert threshold", + ["decision: IT", "phase 4"], + "**Built as the default:** a notification event at 85 percent of the assets volume.\n\n**To answer:** keep, or set a different threshold and who should receive it.", + ), + ( + "Q24", + "Carried marks on routing: computed flag, not copied rows", + ["decision: Seth", "phase 5"], + "When a packet is routed to another department, welds, heat tags and the heat table stay on the packet and show as carried.\n\n**Built as the default:** computed, nothing copied.\n\n**To answer:** keep.", + ), + ( + "Q25", + 'The old "copy marks to the new sheet" button on a change order', + ["decision: Seth", "phase 5"], + "On a page replacement only the heat table placement is copied to the new page; welds and heat tags stay on the VOID page.\n\n**Built as the default:** the manual copy button is not ported.\n\n**To answer:** whether the shop wants it back.", + ), + ( + "Q26", + "Checklist templates: who edits them", + ["decision: Seth", "phase 9"], + "The per-department checklists (weld and non-weld) live in settings with the old lists as defaults.\n\n**Built as the default:** Admin editing arrives in phase 9.\n\n**To answer:** whether Clerical or Q.C. should edit them instead of, or as well as, Admin.", + ), + ( + "Q27", + "Q.C. reject: minimum reason or a well-formed NCR number", + ["decision: Q.C.", "phase 6"], + "**Built as the default:** a reason of at least 8 characters, or an NCR number NCR-YYYY-NNNN, or both; the NCR is linked to pesco-ncr. Validation against pesco-ncr comes later.\n\n**To answer:** keep.", + ), + ( + "Q28", + "A filed packet does not lock the unit's ITP", + ["decision: Q.C.", "phase 6"], + "**Built as the default:** the ITP belongs to the unit, so ITP writes never look at a packet's status.\n\n**To answer:** keep.", + ), + ( + "Q30", + "Removing an ITP block signature: signer or Admin only, while the ITP is open", + ["decision: Q.C.", "phase 6"], + "The old build let any Q.C. editor undo it.\n\n**Built as the default:** the signer or an Admin, while the ITP is open; QN, remarks and customer fields stay.\n\n**To answer:** keep.", + ), + ( + "Q32", + "Prints open as a stamped PDF in a new tab", + ["decision: Seth", "decision: Clerical", "phase 7"], + "The old build pushed page images into the browser print dialog.\n\n**Built as the default:** every print opens as a stamped PDF in a new tab and is printed from the PDF viewer.\n\n**To answer:** keep.", + ), + ( + "Q33", + "The data book is built on the server when the packet is filed", + ["decision: Seth", "decision: Clerical", "phase 7"], + '**Built as the default:** built inside the filing transaction and stored on the seal; a data book that cannot be built fails the filing. Before filing it can be viewed from status complete on, stamped "not yet filed".\n\n**To answer:** keep.', + ), + ( + "Q34", + "Who may read the PDFs and the traceability CSV", + ["decision: Seth", "phase 7"], + "**Built as the default:** every signed-in role that can open the packet, PM included; every read is logged in the append-only print log and the audit.\n\n**To answer:** keep or restrict.", + ), + ( + "Q35", + 'The old "Original packet PDF" print kind is gone', + ["decision: Clerical", "phase 7"], + 'The rebuild has no VISUAL packet PDF; drawing pages come from M-Files or a hand upload, and "Drawing (no markup)" prints them.\n\n**To answer:** confirm nothing is missed.', + ), + ( + "Q36", + "Filing while VISUAL still shows a work order open: warn, not block", + ["decision: Seth", "decision: Clerical", "phase 7"], + "**Built as the default:** a second confirmation and a warning on both routes; the open work orders are kept on the seal and in the log.\n\n**To answer:** keep, or block until VISUAL shows closed.", + ), + ( + "Q37", + "Controlled-copy stamp carries a seal-key signature", + ["decision: Seth", "phase 7"], + "A printed or exported copy can be checked against the server later. Phase 9 adds the Admin screen that checks a copy.\n\n**To answer:** keep; who should be able to run the check.", + ), + ( + "Q39", + "PM reports readable by every signed-in role, every export logged", + ["decision: Seth", "phase 8"], + "**Built as the default:** any signed-in role may read the Project Management reports, not only PS-ProjectMgmt; every export is logged as report exported with scope and period.\n\n**To answer:** keep or restrict.", + ), + ( + "Q40", + "Presence: who is shown and when they drop out", + ["decision: Seth", "phase 8"], + "**Built as the default:** display name and role; locked sessions drop out at once; a closed tab drops within 25 seconds.\n\n**To answer:** keep.", + ), + ( + "Q41", + "KPI numbers follow the project pick only, not the other filters", + ["decision: Seth", "phase 8"], + "**Built as the default:** as the old build. The find bar filters are applied by the server on every board and list.\n\n**To answer:** keep.", + ), + ( + "Q42", + "The stall's Mine flag needs a picked station", + ["decision: Seth", "phase 8"], + "**Built as the default:** Mine is decided from AD roles and the screen; on the stall screen it shows nothing without a picked station.\n\n**To answer:** keep.", + ), + ( + "OPS1", + "docker-1: enlarge the Docker address pool before the production stack", + ["decision: IT", "production"], + "Docker's default pool on docker-1 holds fifteen /16 bridge networks. Ten are used; each Packet Station stack takes two, the Gitea runner one per job, the CI e2e stack two. The pool ran out on 2026-10-17 and every CI e2e job failed until idle networks were removed.\n\n" + '**To do (needs sudo and a Docker restart, a short outage for every stack):** add to /etc/docker/daemon.json `{"default-address-pools":[{"base":"10.200.0.0/16","size":24}]}`, restart Docker. Details in docs/DEPLOY.md.', + ), + ( + "OPS2", + "Move the test stack files from ~/packetstation/test to /etc/packetstation/test", + ["decision: IT"], + "The test stack was set up under /home/spencerm/packetstation/test because the session had no sudo. DEPLOY.md describes /etc/packetstation/ with secrets root:10001 mode 0440. The test secrets are world-readable inside the home folder until moved.\n\n**To do:** move stack.env, secrets/, ca/ and backups/, fix ownership and modes, update ENV_FILE, SECRETS_DIR, CA_BUNDLE_FILE and BACKUP_DIR in stack.env, restart the stack.", + ), +] + + +def api(method: str, path: str, token: str, body: dict | None = None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(BASE + path, data=data, method=method) + req.add_header("Authorization", "token " + token) + req.add_header("Content-Type", "application/json") + req.add_header("Accept", "application/json") + try: + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read().decode() or "null") + except urllib.error.HTTPError as e: + raise SystemExit(f"{method} {path}: HTTP {e.code} {e.read().decode()[:300]}") from None + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--token-file", required=True) + ap.add_argument("--dry-run", action="store_true") + a = ap.parse_args() + if a.dry_run: + for key, title, labels, _ in ISSUES: + print(f"{key:5} {', '.join(labels):40} {title}") + print(f"{len(ISSUES)} issues") + return 0 + + token = Path(a.token_file).read_text(encoding="utf-8").strip() + if not token: + raise SystemExit("token file is empty") + + existing = {lb["name"]: lb["id"] for lb in api("GET", f"/repos/{REPO}/labels?limit=100", token)} + for name, color in LABELS.items(): + if name not in existing: + lb = api("POST", f"/repos/{REPO}/labels", token, {"name": name, "color": "#" + color}) + existing[name] = lb["id"] + print("label created:", name) + + titles = set() + page = 1 + while True: + batch = api("GET", f"/repos/{REPO}/issues?state=all&type=issues&limit=50&page={page}", token) + if not batch: + break + titles.update(i["title"] for i in batch) + page += 1 + + made = 0 + for key, title, labels, body in ISSUES: + if title in titles: + print("exists:", title) + continue + full = ( + body + + f"\n\n---\nPlan reference: {key} in [docs/PLAN.md]({PLAN}) (open questions). Answer here; the build takes the default until then." + ) + issue = api( + "POST", + f"/repos/{REPO}/issues", + token, + {"title": title, "body": full, "labels": [existing[lb] for lb in labels]}, + ) + print(f"created #{issue['number']}: {title}") + made += 1 + print(f"{made} created, {len(ISSUES) - made} already existed") + return 0 + + +if __name__ == "__main__": + sys.exit(main())