# 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())