Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+16 -5
View File
@@ -119,7 +119,7 @@ def test_overview_summary(world: PmWorld) -> None:
slow = [x for x in lines if x.startswith("Slowest step right now: ")]
assert slow and slow[0].startswith(f'Slowest step right now: "{STEP_LABEL["wh_hold"]}", averaging 5')
assert "Needs attention: 1 warehouse hold, 1 open change order, 1 open short, 1 Q.C. reject." in lines
assert "1 packet is over target hours." in lines
assert "1 packet is over target hours on the VISUAL tickets." in lines
def test_overview_attention(world: PmWorld) -> None:
@@ -217,7 +217,7 @@ def test_hours(world: PmWorld) -> None:
assert len(dept) == 1 and "Weld Parts" in dept[0]["cells"][0]
# counted: the running packet, and every packet past review with target hours
assert dept[0]["cells"][1] == "6"
assert dept[0]["cells"][3] == "3.2" # ticket hours: the running packet's VISUAL rows only
assert dept[0]["cells"][3] == "5.2" # ticket hours: the running packet's VISUAL rows only
assert dept[0]["cells"][4] == "27.0" # 6 x 4.5
assert dept[0]["cells"][7] == "1"
over = table(out, "hours_over")["rows"]
@@ -226,7 +226,8 @@ def test_hours(world: PmWorld) -> None:
# 4 h stored plus the running span (2 h when the world was built, more by now)
timer = float(cells[3])
assert 6.0 <= timer < 8.0, cells
assert cells[4] == "3.2" and cells[5] == "4.5" and cells[6] == f"+{timer - 4.5:.1f} h"
# over by the ticket hours (C1), not the timer
assert cells[4] == "5.2" and cells[5] == "4.5" and cells[6] == "+0.7 h"
assert "Andy Welder" in cells[1] or "Stall 1" in cells[1]
workers = {r["cells"][0]: r for r in table(out, "hours_worker")["rows"]}
assert set(workers) == {"Andy Welder", "—"}
@@ -343,13 +344,23 @@ def test_scope_follows_the_project(world: PmWorld) -> None:
assert all(table(q, k)["rows"] == [] for k in TAB_TABLES["quality"])
@pytest.mark.parametrize("sam", ["pm.paula", "clerk.clara", "weld.andy"])
def test_every_signed_in_role_reads(world: PmWorld, sam: str) -> None:
@pytest.mark.parametrize("sam", ["pm.paula", "admin.ann"])
def test_pm_and_admin_read(world: PmWorld, sam: str) -> None:
for what in ENDPOINTS:
r = world.shop(sam).get(f"/api/pm/{what}")
assert r.status_code == 200, (sam, what, r.text)
@pytest.mark.parametrize("sam", ["clerk.clara", "weld.andy", "qc.quinn", "wh.wade", "sup.wes"])
def test_other_roles_are_refused(world: PmWorld, sam: str) -> None:
# change C10: PS-ProjectMgmt and Admin only
for what in [*ENDPOINTS, "export.csv", "report.pdf"]:
r = world.shop(sam).get(f"/api/pm/{what}")
assert r.status_code == 403, (sam, what, r.text)
me = get_ok(world.shop(sam), "/api/auth/me")
assert "pm" not in me.get("screens", []), me
def test_unsigned_is_refused_and_nothing_writes(world: PmWorld) -> None:
c = anon(world.shop)
for what in ENDPOINTS: