Files
spencermandClaude Fable 5.1 3623dca77b
ci / frontend (push) Successful in 43s
ci / server (push) Successful in 11m6s
ci / image (push) Successful in 14s
ci / e2e (push) Failing after 4m3s
scripts: gitea_issues.py drives the tea CLI; 38 decision issues opened on Gitea
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-18 20:50:34 -06:00

363 lines
19 KiB
Python

# ruff: noqa: E501
"""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 subprocess
import sys
import tempfile
from pathlib import Path
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/<stack> 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 tea(*args: str, output: str | None = "json") -> list | dict | str:
cmd = ["tea", *args, "--repo", REPO]
if output:
cmd += ["--output", output]
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
if r.returncode != 0:
raise SystemExit(f"tea {' '.join(args)} failed: {r.stderr.strip()[:400]}")
if output == "json":
return json.loads(r.stdout or "[]")
return r.stdout
def main() -> int:
ap = argparse.ArgumentParser()
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
existing = {lb["name"] for lb in tea("labels", "list")}
for name, color in LABELS.items():
if name not in existing:
tea("labels", "create", "--name", name, "--color", color, output=None)
print("label created:", name)
titles = {i["title"] for i in tea("issues", "list", "--state", "all", "--limit", "500")}
made = 0
for key, title, labels, body in ISSUES:
if title in titles:
print("exists:", title)
continue
footer = (
"Plan reference: "
+ key
+ " in [docs/PLAN.md]("
+ PLAN
+ ") (open questions). Answer here; the build takes the default until then."
)
full = body + "\n\n---\n" + footer + "\n"
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
f.write(full)
path = f.name
try:
out = tea(
"issues",
"create",
"--title",
title,
"--labels",
",".join(labels),
"--description-file",
path,
output=None,
)
finally:
Path(path).unlink(missing_ok=True)
first = (out or "").strip().splitlines()[0] if out else ""
print(f"created: {title} {first[:60]}")
made += 1
print(f"{made} created, {len(ISSUES) - made} already existed")
return 0
if __name__ == "__main__":
sys.exit(main())