All files / web/src/components/create/abacus JobNotices.tsx

100% Statements 62/62
100% Branches 7/7
100% Functions 1/1
100% Lines 62/62

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 631x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 5x 5x 5x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 5x 5x 5x 5x  
// Abacus Studio — the advisory notices on a print job (Gitea #29, THH #429).
//
// A notice is not a failure and not a park. It is the print service saying
// "I let this run without checking X." We submit with `startPolicy: 'auto'`,
// so when the service's bed camera can't reach its vision provider our print
// starts unattended with nothing having looked at the plate — this line is the
// only place that fact surfaces. It renders on every phase the job passes
// through, including `completed`, because a finished print is exactly when
// "nobody checked the bed" explains what you're looking at.
//
// Presentational and hook-free, so every state is reachable from Storybook and
// from a plain render test without staging a live print service.
//
// Deliberately does NOT branch on `cause`. The service writes `detail` as a
// complete sentence including its own remedy ("Top up billing at
// platform.openai.com…"), and anything we compose alongside it can contradict
// it the moment that prose changes. `cause` is the stable machine class, so it
// rides out as a data attribute for tests and support rather than as copy.
 
import { notice as noticeStyle } from '@/components/studio/theme'
import type { JobNotice } from './print-jobs'
 
export interface JobNoticesProps {
  /** The job's `notices[]`, already projected. Empty renders nothing at all. */
  notices: JobNotice[]
}
 
export function JobNotices({ notices }: JobNoticesProps) {
  if (notices.length === 0) return null
  return (
    <>
      {notices.map((notice, i) => (
        <div
          // Composite with the position: the service emits one notice per
          // blocker, and nothing stops two from sharing a code + cause.
          key={`${i}:${notice.code}:${notice.cause ?? ''}`}
          data-component="job-notices"
          data-element="print-job-notice"
          data-code={notice.code}
          data-cause={notice.cause ?? undefined}
          style={{
            ...noticeStyle('warn'),
            // a job row is a compact 11 px list; the notice fragment's 12 px /
            // 8-10 px box roughly doubles each row's height inside it
            padding: '5px 8px 5px 10px',
            fontSize: 11,
            display: 'flex',
            gap: 6,
            whiteSpace: 'normal',
            overflowWrap: 'anywhere',
          }}
        >
          <span aria-hidden="true">⚠</span>
          {/* The service's own sentence, verbatim and never truncated — the
              actionable part is often the tail. `code` is the fallback for a
              service that reported a notice with no prose attached. */}
          <span>{notice.detail ?? notice.code}</span>
        </div>
      ))}
    </>
  )
}