Work-order codes no longer overlap (#42), Materials before Heat map (#44), 60 min idle for Admin and Clerical (#45)
ci / frontend (pull_request) Successful in 54s
ci / server (pull_request) Failing after 12m12s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Skipped

- Traveler: each work-order line shrinks, then clips, to fit its own box; 18 pt gap between boxes
- Work orders tab: card text wraps inside the card, smaller work order, wider cards
- Packet tabs: Materials before Heat map
- Sessions: SESSION_DESK_MINUTES (60) for Admin and Clerical lock and sign-out; /me answers
  lock_minutes and the browser's idle lock follows it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-21 13:16:26 -06:00
co-authored by Claude Opus 5.5
parent b0f6a09173
commit de5b361d85
14 changed files with 119 additions and 36 deletions
+5 -3
View File
@@ -8,7 +8,7 @@ import type { Department, Factor, Me } from "./types";
import { useLocation } from "react-router";
import { screenOf, useEvents } from "./useEvents";
/** The old build's screen lock. The server locks at SESSION_LOCK_MINUTES too; it is the authority. */
/** The old build's screen lock, used until /me says the session's own lock_minutes. The server is the authority. */
export const IDLE_LOCK_MS = 3 * 60_000;
/** A tab hidden for longer than this comes back locked (the old build's rule). */
export const HIDDEN_LOCK_MS = 60_000;
@@ -26,7 +26,7 @@ function goneNotice(err: ApiError): string {
interface AuthProviderProps {
children: ReactNode;
/** Idle time before the local lock. Tests pass a small value. */
/** Idle time before the local lock. Defaults to the session's lock_minutes. Tests pass a small value. */
idleLockMs?: number;
/** How often the idle timer checks. */
idleCheckMs?: number;
@@ -34,7 +34,7 @@ interface AuthProviderProps {
hiddenLockMs?: number;
}
export function AuthProvider({ children, idleLockMs = IDLE_LOCK_MS, idleCheckMs = IDLE_CHECK_MS, hiddenLockMs = HIDDEN_LOCK_MS }: AuthProviderProps) {
export function AuthProvider({ children, idleLockMs: idleLockProp, idleCheckMs = IDLE_CHECK_MS, hiddenLockMs = HIDDEN_LOCK_MS }: AuthProviderProps) {
const [status, setStatusState] = useState<AuthStatus>("loading");
const [me, setMeState] = useState<Me | null>(null);
const [departments, setDepartments] = useState<Department[]>([]);
@@ -193,6 +193,8 @@ export function AuthProvider({ children, idleLockMs = IDLE_LOCK_MS, idleCheckMs
};
}, []);
const sessionLockMin = me?.session.lock_minutes;
const idleLockMs = idleLockProp ?? (sessionLockMin ? sessionLockMin * 60_000 : IDLE_LOCK_MS);
useEffect(() => {
if (status !== "ready") return;
lastActivity.current = Date.now();
+2
View File
@@ -29,6 +29,8 @@ export interface SessionInfo {
expires_at: string;
locked: boolean;
locked_since: string | null;
/** Idle minutes before the screen lock: 60 for Admin and Clerical, SESSION_LOCK_MINUTES otherwise. */
lock_minutes?: number;
device: DeviceRef | null;
}
+2 -2
View File
@@ -35,7 +35,7 @@ import { packetMessage, pillClassFor, WH_STAGES, pillText, progress, whLine, whS
import { useWhMove } from "../warehouse/useWhMove";
type Tab = "drawing" | "heat" | "materials" | "checklist" | "welds" | "qc" | "workorders" | "changes" | "docs" | "history";
const TABS: readonly Tab[] = ["drawing", "heat", "materials", "checklist", "welds", "qc", "workorders", "changes", "docs", "history"];
const TABS: readonly Tab[] = ["drawing", "materials", "heat", "checklist", "welds", "qc", "workorders", "changes", "docs", "history"];
function isTab(v: string | null): v is Tab {
return v !== null && (TABS as readonly string[]).includes(v);
}
@@ -332,8 +332,8 @@ export function PacketPage() {
const ckOpen = p.checklist_items.filter((i) => !p.stage?.checklist?.[i.key]?.done).length;
const tabs: Array<{ id: Tab; label: string; count?: number }> = [
{ id: "drawing", label: "Drawing", count: drawingPages.length },
{ id: "heat", label: "Heat map" },
{ id: "materials", label: "Materials", count: p.bom.length },
{ id: "heat", label: "Heat map" },
{ id: "checklist", label: "Checklist", ...(p.stage ? { count: ckOpen } : {}) },
{ id: "welds", label: "Weld map", count: p.welds.length },
...(hasQc ? [{ id: "qc" as const, label: "Q.C." }] : []),
+1 -1
View File
@@ -351,7 +351,7 @@ export function WorkOrdersTab({ p }: { p: PacketRead }) {
return (
<div key={w.id ?? woKey(w)} className="qr">
<Qr text={code} />
<div>
<div className="txt">
<div className="wo">{w.wo_base}</div>
<div className="small">
Lot <b className="mono">{w.lot}</b> · Seq <b className="mono">{show(w.seq)}</b> {w.op ?? ""}
+3 -2
View File
@@ -337,11 +337,12 @@ input.in.num { text-align: right; font-family: var(--f-mono); }
.act .why { color: var(--amber); }
label.btn input.sr { pointer-events: none; }
.balloon { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; border: 1.5px solid var(--cyan); color: var(--cyan); font-family: var(--f-mono); font-size: 12.5px; font-weight: 600; }
.qrs { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
.qrs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 18px; }
.qr { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; display: flex; gap: 16px; align-items: center; }
.qr .code { background: var(--white); padding: 10px; border-radius: 8px; flex: none; width: 148px; height: 148px; display: grid; place-items: center; color: var(--bg); }
.qr .code canvas { width: 128px !important; height: 128px !important; }
.qr .wo { font-family: var(--f-disp); font-weight: 700; font-size: 26px; color: var(--white); letter-spacing: .03em; }
.qr .txt { min-width: 0; flex: 1; overflow-wrap: anywhere; }
.qr .wo { font-family: var(--f-disp); font-weight: 700; font-size: 22px; color: var(--white); letter-spacing: .03em; line-height: 1.15; }
.pdfv { display: flex; flex-direction: column; gap: 10px; }
.pdfv-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.pdfv-bar .seg button { min-width: 44px; font-weight: 700; }