From 43544738d01c496f302ba0d938c088c5a7776b62 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 10 Oct 2026 11:39:51 +0000 Subject: [PATCH] Add printing submission hours controls --- .../printing-submission-hours/spec.md | 26 + .../features/printing-submission-hours/srd.md | 37 + .../printing-submission-hours/status.md | 18 + .../printing-submission-hours/test-cases.md | 9 + .../(portal)/_components/hacker-printing.tsx | 72 +- .../_components/new-print-job-form.tsx | 20 +- .../printing/printing-queue-workspace.tsx | 12 + .../printing/printing-settings-dialog.tsx | 96 +- packages/api/src/hacker-portal/printing.ts | 31 +- packages/api/src/routers/printing.ts | 67 +- .../src/tests/integration/printing.test.ts | 112 +- packages/api/src/utils/printing/queue.ts | 45 + .../db/drizzle/0062_little_slipstream.sql | 4 + packages/db/drizzle/meta/0062_snapshot.json | 15077 ++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schemas/knight-hacks.ts | 14 +- packages/validators/src/audit.ts | 6 + packages/validators/src/hacker-portal.ts | 9 + packages/validators/src/printing.ts | 19 + 19 files changed, 15661 insertions(+), 20 deletions(-) create mode 100644 .forge/features/printing-submission-hours/spec.md create mode 100644 .forge/features/printing-submission-hours/srd.md create mode 100644 .forge/features/printing-submission-hours/status.md create mode 100644 .forge/features/printing-submission-hours/test-cases.md create mode 100644 packages/db/drizzle/0062_little_slipstream.sql create mode 100644 packages/db/drizzle/meta/0062_snapshot.json diff --git a/.forge/features/printing-submission-hours/spec.md b/.forge/features/printing-submission-hours/spec.md new file mode 100644 index 000000000..6d0f0e146 --- /dev/null +++ b/.forge/features/printing-submission-hours/spec.md @@ -0,0 +1,26 @@ +# Printing Submission Hours + +## Goal + +Admins can control whether hackers may submit new 3D printing requests for a hackathon, because the printing team is only available on-site during limited hours. + +## Users + +- Hackers on the yearly portal printing dashboard. +- Admins with Printing Queue access in Blade. + +## User-visible behavior + +- Hackers can always view their existing print jobs and the queue summary after check-in. +- Hackers can only upload/submit a new print job while printing submissions are currently open. +- When submissions are closed, the dashboard explains that printing is closed and shows the configured window when one exists. +- Admins can set printing submissions open/closed for the selected hackathon. +- Admins can optionally set an open and/or close time, making the control flexible for same-day on-site blocks or manual open-ended sessions. + +## Acceptance criteria + +- A missing printing configuration is safe-closed for new submissions. +- Admins can save open/closed state and optional open/close times from the Printing settings dialog. +- Hacker submit and upload API paths reject new order creation while submissions are closed. +- The hacker dashboard disables the New print action while submissions are closed. +- Existing print queue/status/cancel flows keep working while submissions are closed. diff --git a/.forge/features/printing-submission-hours/srd.md b/.forge/features/printing-submission-hours/srd.md new file mode 100644 index 000000000..97378efb8 --- /dev/null +++ b/.forge/features/printing-submission-hours/srd.md @@ -0,0 +1,37 @@ +# Printing Submission Hours SRD + +## Scope + +Blade printing configuration gains submission availability fields for one hackathon. The 2026 hacker portal consumes the queue DTO to render open/closed state and prevent new submissions. + +## Data model + +Extend `PrintingConfiguration` with: + +- `submissionsEnabled boolean not null default false` +- `submissionsOpenAt timestamptz null` +- `submissionsCloseAt timestamptz null` + +A check constraint requires `submissionsOpenAt < submissionsCloseAt` when both are present. + +## Open-state rule + +`isOpen = submissionsEnabled && (openAt == null || openAt <= now) && (closeAt == null || closeAt > now)`. + +Missing configuration uses defaults: disabled, no window, existing print estimate defaults. + +## API contracts + +- Admin tRPC `printing.getConfiguration` returns submission state. +- Admin tRPC `printing.setSubmissionWindow` saves the enabled flag and nullable open/close timestamps. +- Hacker `listPrintJobs` includes `queue.submissions` with enabled/open/close/isOpen timestamps. +- Hacker `uploadPrintFile` and `submitPrintJob` reject with `PRINTING_CLOSED` when `isOpen` is false. + +## UI contracts + +- Blade admin settings dialog adds a Printing availability section. +- Hacker printing dashboard shows an open/closed notice and disables New print when closed. + +## Rollout + +Migration defaults closed for safety. Admins must explicitly open printing for a hackathon. diff --git a/.forge/features/printing-submission-hours/status.md b/.forge/features/printing-submission-hours/status.md new file mode 100644 index 000000000..2b2aa62c1 --- /dev/null +++ b/.forge/features/printing-submission-hours/status.md @@ -0,0 +1,18 @@ +# Printing Submission Hours Status + +## Phase + +Implementation complete; ready for review. + +## Decisions + +- Missing config defaults to closed for new submissions. +- Admins get a flexible enabled toggle plus optional open/close timestamps. +- Existing jobs remain visible/manageable while new uploads/submits are closed. + +## Validation + +- Passed: `pnpm format`. +- Passed: `pnpm lint` (repo has existing warnings only). +- Passed: `pnpm typecheck`. +- Passed command / skipped by environment: `pnpm --filter=@forge/api test -- src/tests/integration/printing.test.ts` (20 database integration tests skipped because the local test environment did not enable the disposable database suite). diff --git a/.forge/features/printing-submission-hours/test-cases.md b/.forge/features/printing-submission-hours/test-cases.md new file mode 100644 index 000000000..b14b24d82 --- /dev/null +++ b/.forge/features/printing-submission-hours/test-cases.md @@ -0,0 +1,9 @@ +# Printing Submission Hours Test Cases + +- TC-001: Missing printing configuration returns submissions disabled and closed while preserving default queue estimates. +- TC-002: Admin with Printing Queue permission can save submissions enabled with an optional open/close window; configuration reads back the exact state. +- TC-003: Checked-in hacker can submit and upload while submissions are enabled and the current time is within the optional window. +- TC-004: Checked-in hacker cannot upload or submit while submissions are disabled. +- TC-005: Checked-in hacker cannot upload or submit before a future open time or after a close time. +- TC-006: Existing job listing and cancellation still work while submissions are closed. +- TC-007: Hacker portal disables the New print action and explains closed/open-window state when queue submissions are closed. diff --git a/apps/2026/src/app/(portal)/_components/hacker-printing.tsx b/apps/2026/src/app/(portal)/_components/hacker-printing.tsx index 55bd7a344..8d3ea4308 100644 --- a/apps/2026/src/app/(portal)/_components/hacker-printing.tsx +++ b/apps/2026/src/app/(portal)/_components/hacker-printing.tsx @@ -79,6 +79,59 @@ export function HackerPrinting() { return ; } +function formatSubmissionTime(value: string, timeZone: string) { + return new Intl.DateTimeFormat("en-US", { + day: "numeric", + hour: "numeric", + minute: "2-digit", + month: "short", + timeZone, + }).format(new Date(value)); +} + +function PrintingClosedCopy({ + submissions, + timeZone, +}: { + submissions: + | { + closeAt: string | null; + enabled: boolean; + isOpen: boolean; + openAt: string | null; + } + | undefined; + timeZone: string; +}) { + if (!submissions) return <>Checking whether printing is on-site…; + if (!submissions.enabled) { + return ( + <> + Printing is closed right now. Organizers will reopen submissions when + the printers are on-site. + + ); + } + if (submissions.openAt) { + return ( + <>Printing opens {formatSubmissionTime(submissions.openAt, timeZone)}. + ); + } + if (submissions.closeAt) { + return ( + <> + Printing closed {formatSubmissionTime(submissions.closeAt, timeZone)}. + + ); + } + return ( + <> + Printing is closed right now. Check back when organizers reopen + submissions. + + ); +} + function PrintingWorkshop({ timeZone }: { timeZone: string }) { const flow = useHackerPrintingFlow(); const queue = flow.jobsQuery.data?.queue; @@ -104,7 +157,11 @@ function PrintingWorkshop({ timeZone }: { timeZone: string }) {

3D Printing

Send your model, track its progress and collect your print.

- setView("active")} /> + setView("active")} + /> diff --git a/apps/2026/src/app/(portal)/_components/new-print-job-form.tsx b/apps/2026/src/app/(portal)/_components/new-print-job-form.tsx index a5a956925..ae6f4173d 100644 --- a/apps/2026/src/app/(portal)/_components/new-print-job-form.tsx +++ b/apps/2026/src/app/(portal)/_components/new-print-job-form.tsx @@ -37,9 +37,11 @@ function errorMessage(error: unknown, fallback: string) { } export function NewPrintJobForm({ + disabled, flow, onSubmitted, }: { + disabled: boolean; flow: ReturnType; onSubmitted: () => void; }) { @@ -56,6 +58,7 @@ export function NewPrintJobForm({ // file then would race the submit that already holds the list. const submitting = flow.submitMutation.isPending; const canSubmit = + !disabled && description.trim() !== "" && uploadedIds.length > 0 && !uploading && @@ -69,6 +72,10 @@ export function NewPrintJobForm({ } function addFiles(list: FileList) { + if (disabled) { + toast.error("Printing is not accepting new requests right now."); + return; + } const room = PRINTING.MAX_PRINT_JOB_FILES - files.length; const picked = Array.from(list).slice(0, Math.max(room, 0)); if (list.length > picked.length) { @@ -106,6 +113,10 @@ export function NewPrintJobForm({ async function submit() { try { + if (disabled) { + toast.error("Printing is not accepting new requests right now."); + return; + } const job = await flow.submit({ description: description.trim(), fileIds: uploadedIds, @@ -132,7 +143,7 @@ export function NewPrintJobForm({ }} > - @@ -196,13 +207,16 @@ export function NewPrintJobForm({

) : null} {files.length < PRINTING.MAX_PRINT_JOB_FILES ? ( -