From 3b4dbccb43ab482a2f3fadefca2c2b6458d38571 Mon Sep 17 00:00:00 2001 From: Adrian Osorio Blanchard Date: Sat, 10 Oct 2026 10:47:21 -0400 Subject: [PATCH 1/2] registered printing category endpoint in api contracts Co-authored-by: Codex --- .forge/features/printing-categories/status.md | 19 ++++++++++++------- .../__snapshots__/api-surface.test.ts.snap | 1 + packages/api/src/utils/audit/coverage.ts | 3 ++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.forge/features/printing-categories/status.md b/.forge/features/printing-categories/status.md index a3d555ea8..0cd7f6259 100644 --- a/.forge/features/printing-categories/status.md +++ b/.forge/features/printing-categories/status.md @@ -1,6 +1,8 @@ # Status -Phase: templates approved; production migration applied; publication and live verification in progress. Branch: codex/printing-categories. +Phase: production rollout complete; API contract bookkeeping correction awaiting PR review. Follow-up branch: codex/printing-category-ci. + +Tracking issue: [#617](https://github.com/KnightHacks/forge/issues/617). ## Decisions @@ -9,7 +11,7 @@ Phase: templates approved; production migration applied; publication and live ve - Reused the IX arrival email design and safe React Email compiler; no new dependencies. Every submit, category change, cancellation, organizer status/note change, and manual timing change sends an update. No-op saves and participant replays do not send twice. - Removed automatic equal-duration estimates from hacker notices and organizer summaries. Explicit organizer estimates remain available and are shown on the hacker dashboard. - User selected a one-time reminder after launch, requested screenshots before any sends, then approved the Shinies-branded previews and requested merging to main. Sending is authorized after the live category flow and logo asset are verified. -- Added the existing Shinies wordmark prominently above both email headings, plus partner attribution in HTML and plain text. A PNG export will be served by Blade; verify that asset is publicly available after deployment and before any send. Updated desktop/mobile previews were inspected. +- Added the existing Shinies wordmark prominently above both email headings, plus partner attribution in HTML and plain text. Blade serves the PNG export; its public response matched the release asset before sending. Updated desktop/mobile previews were inspected. - Reminder previews counts by default; live sending requires --send and configured production email mode. Atomic attempts prevent repeated/concurrent sends. Failures remain claimed for operator inspection because delivery may be ambiguous. ## Validation @@ -26,13 +28,16 @@ Phase: templates approved; production migration applied; publication and live ve - Stopped both local development servers and removed the isolated browser-test database. - Blade and KHIX production builds passed. The first KHIX build lacked local portal environment values; rerunning with the registered production portal client/origin supplied to that build succeeded without changing environment files. - Production preflight verified 0063 was the only pending migration. Applied it successfully; all 24 jobs, 62 files, and 1 configuration remain. The three new nullable columns exist and no migrations remain pending. -- Reminder preview: 14 recipients with 20 eligible uncategorized jobs. No emails sent yet. -- The previous main CI test run had an unhandled disposable-database teardown error in project-room-filter.test.ts after all tests passed. Its 3 local tests pass; the failed CI job was rerun before publication. +- Reminder sent once after live verification: 14 accepted by the email provider, 0 failed, 0 skipped, covering 20 uncategorized jobs. The subsequent preview returned 0 eligible recipients and 0 jobs; no resend was attempted. +- The previous main CI test run had an unhandled disposable-database teardown error in project-room-filter.test.ts after all tests passed. Its 3 local tests and the CI rerun passed before publication. +- Main CI run 38060417381 found two missing contract entries for printing.updateCategory: the API surface snapshot and audit coverage declaration. Added both, with hybrid audit classification matching its no-op behavior; the focused 11 tests pass. Root format, lint, and typecheck passed after the correction. No runtime behavior changed in this follow-up. +- Live KHIX shows project priority and variable print times. Blade shows uncategorized requests and both category choices. The public Shinies PNG returns HTTP 200 and matches the committed asset. ## Rollout 1. Completed: applied additive 0063_first_jean_grey to production and verified preserved job/file counts. -2. Publish verified commits to main under the user's approval; verify Blade and KHIX deployments expose required categories and the Shinies email PNG. -3. After live verification, run packages/api/scripts/send-print-category-reminders.ts --send in production email mode. Report accepted/failed/skipped totals and inspect failures; never automatically retry ambiguous sends. +2. Completed: published through 349574e5 under the user's earlier merge approval and verified the live deployments and logo. +3. Completed: sent the authorized one-time reminder in production email mode, then verified no eligible reminders remain. +4. Open the contract bookkeeping correction as a PR; the user requested PR review instead of further direct pushes to main. -No PR opened. Unrelated pre-existing local files left untouched. +The Hacker’s Guide iframe was preserved unchanged. The user confirmed the blank state occurred only in Codex; it subsequently rendered in Codex too. No guide code changes were needed. Unrelated pre-existing local files were left untouched. diff --git a/packages/api/src/tests/root/__snapshots__/api-surface.test.ts.snap b/packages/api/src/tests/root/__snapshots__/api-surface.test.ts.snap index 70835d964..bcd49ed20 100644 --- a/packages/api/src/tests/root/__snapshots__/api-surface.test.ts.snap +++ b/packages/api/src/tests/root/__snapshots__/api-surface.test.ts.snap @@ -334,6 +334,7 @@ exports[`client-facing API surface > exposes exactly the documented procedure pa "printing.setChannel", "printing.setEstimateSettings", "printing.setEstimatedReadyAt", + "printing.updateCategory", "printing.updateStatus", "profilePicture.getProfilePicture", "profilePicture.saveMemberProfilePicture", diff --git a/packages/api/src/utils/audit/coverage.ts b/packages/api/src/utils/audit/coverage.ts index 6441e14d6..b6ed5f3cf 100644 --- a/packages/api/src/utils/audit/coverage.ts +++ b/packages/api/src/utils/audit/coverage.ts @@ -183,7 +183,8 @@ export const HYBRID_ADMIN_PROCEDURES = [ "forms.finalizeUpload", "forms.getAttachmentDownload", "forms.getLegacyAttachmentDownload", - // Saving the same status and note again changes nothing and writes no event. + // Saving unchanged print details writes no event. + "printing.updateCategory", "printing.updateStatus", ] as const; From df1f13b7321b9db1df55381182edd8dc54a145cc Mon Sep 17 00:00:00 2001 From: Adrian Osorio Blanchard Date: Sat, 10 Oct 2026 13:05:42 -0400 Subject: [PATCH 2/2] clarified printing queues and added duration timers Co-authored-by: Codex --- .forge/features/printing-categories/spec.md | 8 + .forge/features/printing-categories/srd.md | 10 + .forge/features/printing-categories/status.md | 20 +- .../printing-categories/test-cases.md | 5 + .../_components/hacker-printing.module.css | 25 +++ .../(portal)/_components/hacker-printing.tsx | 42 +++- .../_components/new-print-job-form.tsx | 6 +- .../_components/printing-guidelines.tsx | 29 +++ .../admin/printing/print-duration-fields.tsx | 80 ++++++++ .../admin/printing/print-job-dialog.tsx | 183 +++++++----------- .../admin/printing/print-queue-format.ts | 6 - .../printing/printing-queue-workspace.tsx | 45 ++++- .../tests/e2e/printing-availability.spec.ts | 49 ++++- .../src/tests/e2e/printing-portal.spec.ts | 57 +++++- packages/api/src/hacker-portal/printing.ts | 2 + packages/api/src/routers/printing.ts | 37 +++- .../src/tests/integration/printing.test.ts | 75 +++++++ packages/api/src/utils/printing/queue.ts | 13 +- packages/consts/src/printing.ts | 13 ++ packages/ui/src/countdown.tsx | 37 ++++ packages/validators/src/audit.ts | 2 +- packages/validators/src/hacker-portal.ts | 11 ++ packages/validators/src/printing.ts | 14 ++ .../validators/src/tests/printing.test.ts | 23 +++ 24 files changed, 648 insertions(+), 144 deletions(-) create mode 100644 apps/2026/src/app/(portal)/_components/printing-guidelines.tsx create mode 100644 apps/blade/src/app/_components/admin/printing/print-duration-fields.tsx create mode 100644 packages/ui/src/countdown.tsx diff --git a/.forge/features/printing-categories/spec.md b/.forge/features/printing-categories/spec.md index 159fc6fd7..d4e36dfb0 100644 --- a/.forge/features/printing-categories/spec.md +++ b/.forge/features/printing-categories/spec.md @@ -9,3 +9,11 @@ Submissions, category changes, cancellations, organizer status/note changes, and Status and reminder emails use the existing Knight Hacks IX arrival email artwork, navy/purple palette, serif typography, and yellow action button. Prominently display the Shinies wordmark above the message heading and name Shinies as the printing partner in text, including the plain-text email. Include status, description, category, organizer note, and tracker link. Do not promise automatic print times. Keep non-IX emails event-neutral. The user selected sending one reminder after launch, not an organizer send button. Send one email per recipient with eligible uncategorized requests, linking to the category editor. Exclude printing, completed, cancelled, and categorized requests. Never reset status or submission time. + +## Session limits, materials, and queue clarity + +Show the Shinies inventory for Knight Hacks IX: black PLA, black PETG, white PLA, transparent PETG, pink PLA, brown PLA, silver silk PLA, and gold silk PLA. Hackers specify their preferred filament in the existing print description. State that sessions are limited to one hour so everyone has a chance; oversized requests may be cancelled. This is a maximum, not a fixed duration for every model. + +Show the hackathon-project and personal queues with their counts. Show the active project and personal totals (for example, “10 hackathon projects · 5 personal prints”) above the queue. Each waiting request shows its overall place in line. Explain that projects have priority, running prints finish first, and new project requests can move personal prints back. Uncategorized legacy requests remain clearly labeled. Needs-clarification requests are on hold and do not have a queue position until an organizer returns them to Received. + +Replace the organizer's exact-date/time control with a duration entered in hours and minutes (for example, 1 hour 30 minutes). Longer-than-one-hour estimates are allowed for accurate organizer planning, with a visible session-limit warning. Entering Printing starts the timer using the server's time. Editing the duration of a running print recalculates from its original start; note edits and repeated saves never restart it. Both dashboards show a live countdown and approximate finish time. An expired estimate does not automatically mark a print ready; organizers still confirm completion and pickup. Status, note, and timer changes continue to send the existing branded email updates. diff --git a/.forge/features/printing-categories/srd.md b/.forge/features/printing-categories/srd.md index 5c27f9dd0..917aab147 100644 --- a/.forge/features/printing-categories/srd.md +++ b/.forge/features/printing-categories/srd.md @@ -11,3 +11,13 @@ Render status/reminder email with Forge's existing safe React Email TSX compiler The one-time reminder command previews counts by default and requires --send plus production email mode to send. Claim eligible jobs atomically before provider calls; record attempted and accepted timestamps. Claims prevent concurrent/repeated invocations from sending again. Ambiguous failures remain attempted and require operator inspection, never automatic retries. Recheck eligibility when claiming, and consolidate multiple jobs by recipient email. No live email during testing. Rollout: generate/review additive migration; verify on a disposable local DB; migrate production before deploying consumers. User requested feature and email changes, and explicitly selected sending after launch. Verify deployed category UI before running reminder command. Rollback code retains added nullable columns and existing jobs. Do not drop migration history. + +## Branch-only session follow-up + +Work stays on codex/printing-category-ci and PR #618. No main push, merge, production migration, or bulk email send is part of this follow-up. + +Reuse PrintJob.statusChangedAt as the printing start and estimatedReadyAt as the saved finish estimate. Extend updateStatus with optional durationMinutes; the new organizer UI requires a duration when selecting Printing, while existing clients may still start an untimed print. Validate whole minutes from 1 to the existing 600-minute estimate bound. The user clarified that estimates such as 90 minutes must be allowed even though the published session policy is one hour. Store start and calculated finish atomically under the existing job row lock. Existing running jobs can still receive note updates without a new timer. Calculate duration edits from the original start and audit the changed estimate; one mutation emits at most one notification. Clear the timer when a print leaves Printing. Keep the old ready-time endpoint for already deployed clients, but remove its control from the UI. No schema changes are needed. + +Expose active category counts and on-hold count on the queue DTO, with null defaults for older cached responses (unknown counts must not appear as zero). Keep the existing overall position. Derive all positions from the existing server ordering. Needs-clarification jobs are excluded from positions. Reuse an app-agnostic Countdown primitive in @forge/ui across Blade and KHIX. Countdowns use persisted timestamps, update locally, clean up interval subscriptions, and never perform automatic status mutations or repeated notifications. + +The exact filament inventory is explicitly scoped to the current KHIX event, beside its existing Shinies branding; this limited event update does not add an inventory-management schema. Show the supplied one-hour session policy in the KHIX request flow; warn organizers when their estimate exceeds it, while still allowing an honest estimate. Preserve existing styling and validate desktop and 320px views. diff --git a/.forge/features/printing-categories/status.md b/.forge/features/printing-categories/status.md index 0cd7f6259..979d16822 100644 --- a/.forge/features/printing-categories/status.md +++ b/.forge/features/printing-categories/status.md @@ -1,9 +1,11 @@ # Status -Phase: production rollout complete; API contract bookkeeping correction awaiting PR review. Follow-up branch: codex/printing-category-ci. +Phase: session policy, filament guidance, queue clarity, and duration timers implemented and locally validated on codex/printing-category-ci for PR #618, with the unrelated post-build typecheck limitation below. Existing production category/email rollout is complete. Tracking issue: [#617](https://github.com/KnightHacks/forge/issues/617). +PR: [#618](https://github.com/KnightHacks/forge/pull/618). Its initial contract correction passed CI and CodeRabbit review. The new requested behavior remains branch-only pending updated review. + ## Decisions - Project and personal categories are required on new requests. Existing requests stay null until classified. @@ -13,6 +15,20 @@ Tracking issue: [#617](https://github.com/KnightHacks/forge/issues/617). - User selected a one-time reminder after launch, requested screenshots before any sends, then approved the Shinies-branded previews and requested merging to main. Sending is authorized after the live category flow and logo asset are verified. - Added the existing Shinies wordmark prominently above both email headings, plus partner attribution in HTML and plain text. Blade serves the PNG export; its public response matched the release asset before sending. Updated desktop/mobile previews were inspected. - Reminder previews counts by default; live sending requires --send and configured production email mode. Atomic attempts prevent repeated/concurrent sends. Failures remain claimed for operator inspection because delivery may be ambiguous. +- Follow-up: display the eight specified IX filaments and the 60-minute fairness policy on the hacker page and submission form. Requests exceeding the policy may be cancelled; staff review model size. +- Follow-up: show active project/personal counts and overall queue positions, with held requests outside the queue. Running jobs finish first, then waiting projects take priority, with oldest-first ordering within categories. +- Follow-up: organizers enter hours and minutes when starting Printing. The server computes the finish estimate from the persisted start; duration edits keep that start, and note-only or repeated saves never restart the timer. A 90-minute estimate is allowed with a 60-minute policy warning, as clarified by the user. Expiry never automatically marks a job ready. +- Reuse the existing estimatedReadyAt/statusChangedAt fields and status notification flow. Keep the older timestamp endpoint compatible; no schema, dependency, or environment changes are needed. Keep this follow-up on the existing PR branch; no production changes or reminder resends. + +## Session-policy follow-up validation + +- Printing API suite: 24 tests passed against disposable local Postgres, including 90-minute duration, idempotency, note-only edits, timer adjustments, audit fields, notification delivery calls, and held-job counts. The initial focused API regression run passed 41 tests across 5 files. +- Validators: 350 tests passed. Hacker SDK: 57. KHIX: 190. Email: 116. Blade queue formatting: 3. +- Organizer and hacker browser tests passed at desktop and 320px mobile (4 tests). Confirmed duration persistence after reload, 1h30m input and policy warning, category counts, overall position, all filaments, preserved closed-state drafts, and display-only timer expiry. +- Inspected real browser screenshots using synthetic fixture data. Local previews: output/playwright/printing-session-policy/. Both organizer and hacker mobile screenshots were recaptured and checked after final visual adjustments. +- Changed React analysis: 7 files, 7 components, 0 failures. Root format and lint passed (existing warnings). Root typecheck passed before production builds (33 tasks); afterward `pnpm typecheck` fails solely in `.next/types/app/api/admin/resume-bundle/route.ts:42` with TS2344 because the existing `GET(request?: Request)` signature is incompatible with generated `ParamCheck`. The source route is identical to origin/main and remains outside this printing change. The other 32 typecheck tasks passed. +- Blade and KHIX production builds passed, with local database and portal overrides. Both dev servers were stopped and the isolated browser database was removed. These builds skip their own type validation; the separate post-build check above records the existing limitation. +- Commit hook formatting and lint passed; its typecheck reproduced the same unrelated resume-bundle error. Used a one-command hook override after recording that failure; no check configuration or unrelated route was changed. ## Validation @@ -38,6 +54,6 @@ Tracking issue: [#617](https://github.com/KnightHacks/forge/issues/617). 1. Completed: applied additive 0063_first_jean_grey to production and verified preserved job/file counts. 2. Completed: published through 349574e5 under the user's earlier merge approval and verified the live deployments and logo. 3. Completed: sent the authorized one-time reminder in production email mode, then verified no eligible reminders remain. -4. Open the contract bookkeeping correction as a PR; the user requested PR review instead of further direct pushes to main. +4. PR #618 is open with the requested filament guidance, one-hour policy, explicit queue positions, and duration timer. Keep it on the existing branch for review; do not merge or push main. The Hacker’s Guide iframe was preserved unchanged. The user confirmed the blank state occurred only in Codex; it subsequently rendered in Codex too. No guide code changes were needed. Unrelated pre-existing local files were left untouched. diff --git a/.forge/features/printing-categories/test-cases.md b/.forge/features/printing-categories/test-cases.md index 29ebb4f78..3e3b19946 100644 --- a/.forge/features/printing-categories/test-cases.md +++ b/.forge/features/printing-categories/test-cases.md @@ -8,3 +8,8 @@ - Every organizer status and note change reaches the email gateway with current status, description, category, and IX design. No-op saves send nothing. Discord failure does not block email or committed status. Provider failure is reported. - Reminder targets only editable uncategorized requests, groups multiple jobs into one email, skips missing contacts/portal, and cannot send twice on repeat/concurrent calls. Failed/ambiguous delivery remains visible without automatic retries. - Desktop and 320px mobile: required new category, visible legacy prompt, saved category while closed, project priority copy; no fixed print-time claims. Preview email HTML/text for escaped content and correct links/artwork. + +- Duration accepts whole minutes 1–600, including a 90-minute estimate; zero, fractions, and durations over 600 fail. The organizer UI requests hours/minutes when starting Printing and persists a server-calculated finish; category priority and permission checks remain intact. +- Repeated status/duration saves are no-ops. Editing a note preserves start/finish; changing duration preserves the original start and sends one update. Leaving Printing clears the timer. Expiry does not change status. +- Mixed project/personal/uncategorized jobs expose correct overall ranks and category totals, with running prints first. Held jobs have no rank and appear in the on-hold count. Both dashboards label each number unambiguously. +- Both dashboards show the running timer and honest expired-estimate copy. KHIX dashboard and new-request form display all eight filaments and the one-hour maximum. No datetime-local control remains in the organizer workflow. diff --git a/apps/2026/src/app/(portal)/_components/hacker-printing.module.css b/apps/2026/src/app/(portal)/_components/hacker-printing.module.css index 5ada92190..86835c443 100644 --- a/apps/2026/src/app/(portal)/_components/hacker-printing.module.css +++ b/apps/2026/src/app/(portal)/_components/hacker-printing.module.css @@ -160,6 +160,31 @@ font-size: 0.875rem; line-height: 1.65; } +.guidelines { + display: grid; + gap: 1rem; + padding-block: 1rem; + border-block: 1px solid var(--print-line); + font-size: 0.9375rem; + line-height: 1.65; +} +.filaments { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1.5rem; + margin-block: 0.5rem; + padding-left: 1.1rem; + list-style: disc; + color: var(--print-muted); + font-size: 0.875rem; +} +.queueCounts { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1.5rem; + margin-block: 0.5rem; + font-variant-numeric: tabular-nums; +} .jobs { display: grid; min-width: 0; diff --git a/apps/2026/src/app/(portal)/_components/hacker-printing.tsx b/apps/2026/src/app/(portal)/_components/hacker-printing.tsx index 4972b95bc..00657bc10 100644 --- a/apps/2026/src/app/(portal)/_components/hacker-printing.tsx +++ b/apps/2026/src/app/(portal)/_components/hacker-printing.tsx @@ -8,6 +8,7 @@ import { ArrowUpRight, Box, Clock3, Paperclip, Printer } from "lucide-react"; import type { HackerPrintJobDto } from "@forge/hacker-sdk"; import { PRINTING } from "@forge/consts"; import { Button } from "@forge/ui/button"; +import { Countdown } from "@forge/ui/countdown"; import { Dialog, DialogContent, @@ -28,6 +29,7 @@ import { StatusStage } from "./khix-dashboard"; import dashboardStyles from "./khix-dashboard.module.css"; import { NewPrintJobForm } from "./new-print-job-form"; import { PrintCategoryEditor } from "./print-category-choices"; +import { PrintingGuidelines } from "./printing-guidelines"; export function HackerPrinting() { const { dashboard, dashboardQuery } = useHackerDashboardFlow(); @@ -138,13 +140,33 @@ function PrintingWorkshop({ timeZone }: { timeZone: string }) { "Checking the print queue…" )}

+ {queue?.categoryCounts ? ( +
+ {queue.categoryCounts.project} hackathon projects + {queue.categoryCounts.personal} personal prints + {queue.categoryCounts.uncategorized > 0 ? ( + + {queue.categoryCounts.uncategorized} awaiting category + + ) : null} +
+ ) : null} + {queue?.onHoldCount ? ( +

+ {queue.onHoldCount} on hold for clarification, outside the queue. +

+ ) : null}

- Hackathon project prints have priority. Print times vary depending - on the model. + Hackathon projects take priority. Running prints finish first; each + category is oldest first. Your number is your overall place in line. + New projects may move personal prints back. Print times vary + depending on the model.

+ +