> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a lab status report for a project

> Return a point-in-time lab operations report, rendered as Markdown.

Covers momentum over the trend window, utilization (whole lab, per site,
per program, and against yesterday), channels released in the recent past,
channels forecast to free up soon, the planned measurements queued next,
plans that should already have started, equipment service — channels and
whole cyclers out, calibration falling due, and booked visits — and healthy
channels sitting idle. The response
carries every section as structured data *and* the whole thing as a
Markdown document in ``markdown``, so a caller can convert it to PDF or
paste it into a message without re-deriving anything.

This endpoint only builds the report; it does not deliver it anywhere.



## OpenAPI

````yaml https://api.ionworks.com/openapi.json get /projects/{project_id}/lab/report
openapi: 3.1.0
info:
  title: FastAPI
  version: 0.1.0
servers:
  - url: https://api.ionworks.com
    description: Production
security: []
paths:
  /projects/{project_id}/lab/report:
    get:
      tags:
        - Lab
      summary: Build a lab status report for a project
      description: >-
        Return a point-in-time lab operations report, rendered as Markdown.


        Covers momentum over the trend window, utilization (whole lab, per site,

        per program, and against yesterday), channels released in the recent
        past,

        channels forecast to free up soon, the planned measurements queued next,

        plans that should already have started, equipment service — channels and

        whole cyclers out, calibration falling due, and booked visits — and
        healthy

        channels sitting idle. The response

        carries every section as structured data *and* the whole thing as a

        Markdown document in ``markdown``, so a caller can convert it to PDF or

        paste it into a message without re-deriving anything.


        This endpoint only builds the report; it does not deliver it anywhere.
      operationId: get_lab_report_projects__project_id__lab_report_get
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            title: Project Id
        - name: sections
          in: query
          required: false
          schema:
            anyOf:
              - type: array
                items:
                  $ref: '#/components/schemas/LabReportSection'
              - type: 'null'
            description: >-
              Sections to include. Repeat the parameter for several. Omit for
              the full report.
            title: Sections
          description: >-
            Sections to include. Repeat the parameter for several. Omit for the
            full report.
        - name: recently_stopped_hours
          in: query
          required: false
          schema:
            type: integer
            maximum: 2160
            minimum: 1
            description: Lookback for stopped channels
            default: 24
            title: Recently Stopped Hours
          description: Lookback for stopped channels
        - name: stopping_soon_hours
          in: query
          required: false
          schema:
            type: integer
            maximum: 2160
            minimum: 1
            description: Forecast window for channels freeing up
            default: 24
            title: Stopping Soon Hours
          description: Forecast window for channels freeing up
        - name: idle_thresholds_days
          in: query
          required: false
          schema:
            anyOf:
              - type: array
                items:
                  type: integer
                  maximum: 365
                  minimum: 1
              - type: 'null'
            description: >-
              Idle thresholds in days, e.g. 2 and 3. Channels past the lowest
              are listed; the rest become counts. Defaults to 2 and 3.
            title: Idle Thresholds Days
          description: >-
            Idle thresholds in days, e.g. 2 and 3. Channels past the lowest are
            listed; the rest become counts. Defaults to 2 and 3.
        - name: queue_limit
          in: query
          required: false
          schema:
            type: integer
            maximum: 200
            minimum: 1
            description: Maximum queued measurements to list
            default: 20
            title: Queue Limit
          description: Maximum queued measurements to list
        - name: queue_horizon_days
          in: query
          required: false
          schema:
            type: integer
            maximum: 365
            minimum: 1
            description: How far ahead the queue looks
            default: 7
            title: Queue Horizon Days
          description: How far ahead the queue looks
        - name: service_horizon_days
          in: query
          required: false
          schema:
            type: integer
            maximum: 365
            minimum: 1
            description: How far ahead booked service is reported
            default: 30
            title: Service Horizon Days
          description: How far ahead booked service is reported
        - name: trend_window_days
          in: query
          required: false
          schema:
            type: integer
            maximum: 90
            minimum: 1
            description: Length of the momentum trend window
            default: 7
            title: Trend Window Days
          description: Length of the momentum trend window
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LabReport'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    LabReportSection:
      type: string
      enum:
        - highlights
        - utilization
        - recently_stopped
        - stopping_soon
        - past_due
        - queue
        - unscheduled_requests
        - maintenance
        - idle_channels
      title: LabReportSection
      description: >-
        The selectable sections of a lab report.


        Used both as the ``sections`` query parameter on the report endpoint and
        as

        the key for the corresponding field on :class:`LabReport`.
    LabReport:
      properties:
        project_id:
          type: string
          title: Project Id
          description: Project the report covers
        project_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Project Name
          description: Project name
        organization_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Organization Name
          description: Organization the project belongs to
        prepared_for_email:
          anyOf:
            - type: string
            - type: 'null'
          title: Prepared For Email
          description: Email of the user the report was generated for
        generated_at:
          type: string
          format: date-time
          title: Generated At
          description: Server time the report was generated (UTC)
        parameters:
          $ref: '#/components/schemas/LabReportParameters'
          description: The parameters this report was built with
        highlights:
          anyOf:
            - $ref: '#/components/schemas/HighlightsSection'
            - type: 'null'
        utilization:
          anyOf:
            - $ref: '#/components/schemas/UtilizationSection'
            - type: 'null'
        recently_stopped:
          anyOf:
            - $ref: '#/components/schemas/RecentlyStoppedSection'
            - type: 'null'
        stopping_soon:
          anyOf:
            - $ref: '#/components/schemas/StoppingSoonSection'
            - type: 'null'
        past_due:
          anyOf:
            - $ref: '#/components/schemas/PastDueSection'
            - type: 'null'
        queue:
          anyOf:
            - $ref: '#/components/schemas/QueueSection'
            - type: 'null'
        unscheduled_requests:
          anyOf:
            - $ref: '#/components/schemas/UnscheduledRequestsSection'
            - type: 'null'
        maintenance:
          anyOf:
            - $ref: '#/components/schemas/MaintenanceSection'
            - type: 'null'
        idle_channels:
          anyOf:
            - $ref: '#/components/schemas/IdleChannelsSection'
            - type: 'null'
        markdown:
          type: string
          title: Markdown
          description: The full report rendered as Markdown
      type: object
      required:
        - project_id
        - generated_at
        - parameters
        - markdown
      title: LabReport
      description: >-
        A rendered lab operations report plus the data behind it.


        ``markdown`` is the whole report as a document (the intended payload for

        PDF conversion or messaging); the structured sections are the same
        content

        unformatted, for consumers that want to re-render or post-process it.

        Sections not requested are ``None``.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    LabReportParameters:
      properties:
        sections:
          items:
            $ref: '#/components/schemas/LabReportSection'
          type: array
          title: Sections
          description: Sections included in this report
        recently_stopped_hours:
          type: integer
          title: Recently Stopped Hours
          description: Lookback window for the recently-stopped section
        stopping_soon_hours:
          type: integer
          title: Stopping Soon Hours
          description: Forecast window for the stopping-soon section
        idle_thresholds_days:
          items:
            type: integer
          type: array
          title: Idle Thresholds Days
          description: Idle thresholds reported, in days, ascending
        queue_limit:
          type: integer
          title: Queue Limit
          description: Maximum queued measurements listed
        queue_horizon_days:
          type: integer
          title: Queue Horizon Days
          description: How far ahead the queue section looks
        service_horizon_days:
          type: integer
          title: Service Horizon Days
          description: How far ahead booked service visits were reported
          default: 30
        trend_window_days:
          type: integer
          title: Trend Window Days
          description: Length of the highlights trend window
      type: object
      required:
        - sections
        - recently_stopped_hours
        - stopping_soon_hours
        - idle_thresholds_days
        - queue_limit
        - queue_horizon_days
        - trend_window_days
      title: LabReportParameters
      description: The knobs the report was built with, echoed back for reproducibility.
    HighlightsSection:
      properties:
        window_days:
          type: integer
          title: Window Days
          description: Length of the trend window in days
        daily_utilization:
          items:
            $ref: '#/components/schemas/DailyUtilizationPoint'
          type: array
          title: Daily Utilization
          description: One point per day, oldest first
        best_day:
          anyOf:
            - $ref: '#/components/schemas/DailyUtilizationPoint'
            - type: 'null'
          description: The highest-utilization point in the window
        tests_completed:
          type: integer
          title: Tests Completed
          description: Tests that finished within the window
          default: 0
        tests_started:
          type: integer
          title: Tests Started
          description: Tests that started within the window
          default: 0
        channel_hours_used:
          type: number
          title: Channel Hours Used
          description: >-
            Total channel-hours of testing delivered inside the window — the
            lab's actual output, independent of how many channels it has.
          default: 0
        channel_hours_delta:
          anyOf:
            - $ref: '#/components/schemas/PeriodDelta'
            - type: 'null'
          description: Channel-hours delivered against the previous window
        tests_started_delta:
          anyOf:
            - $ref: '#/components/schemas/PeriodDelta'
            - type: 'null'
          description: Tests started against the previous window
        tests_completed_delta:
          anyOf:
            - $ref: '#/components/schemas/PeriodDelta'
            - type: 'null'
          description: Tests completed against the previous window
        delta_window:
          anyOf:
            - $ref: '#/components/schemas/ComparisonWindow'
            - type: 'null'
          description: Which two windows every delta above compares
      type: object
      required:
        - window_days
      title: HighlightsSection
      description: >-
        Momentum: what the lab produced over the trend window, and how hard it
        ran.


        Two subjects only — **output** (channel-hours, tests started and
        finished)

        and the **utilization trend** — both reconstructed by replaying
        measurement

        intervals. Equipment service lives in :class:`MaintenanceSection`
        instead,

        beside the channels currently down: repairs are a different subject from

        output, and the multi-period repair ladder reaches further back than
        this

        section's window, so presenting it under a "last N days" heading was a

        contradiction.


        Every point divides by today's channel count, because ``channels``
        itself

        keeps no history: a channel added last week is in the denominator of
        samples

        from before it existed. That makes the *shape* of the series trustworthy
        and

        its absolute values approximate whenever equipment moved inside the
        window —

        and is why the trend window is capped.
    UtilizationSection:
      properties:
        lab:
          $ref: '#/components/schemas/UtilizationRow'
          description: The project's equipment as a whole
        comparisons:
          items:
            $ref: '#/components/schemas/UtilizationComparison'
          type: array
          title: Comparisons
          description: >-
            Headline utilization against several points in the past, shortest
            lookback first. Empty when it could not be reconstructed.
        by_site:
          items:
            $ref: '#/components/schemas/UtilizationRow'
          type: array
          title: By Site
          description: One row per site, ordered by name
        by_program:
          items:
            $ref: '#/components/schemas/ProgramUtilizationRow'
          type: array
          title: By Program
          description: One row per program in use, busiest first
      type: object
      required:
        - lab
      title: UtilizationSection
      description: >-
        Utilization for the whole project, by site and by program, vs yesterday.


        The day-over-day comparison is reconstructed rather than stored: there
        is no

        utilization history table, so yesterday's figure is derived by replaying
        the

        measurement intervals that were open 24 hours ago. Its denominator is

        *today's* channel count, because the equipment tables carry no history

        either — so a lab that added channels overnight will see a small
        artificial

        dip. Over a single day that error is negligible; over months it would
        not

        be, which is why nothing here reaches further back than the trend
        window.
    RecentlyStoppedSection:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/StoppedMeasurementRow'
          type: array
          title: Rows
          description: Finished runs, most recently ended first
        beyond_cap:
          type: integer
          title: Beyond Cap
          description: >-
            Runs the display cap excluded, exactly: the query counts the whole
            window server-side, so this is how many more finished, not merely
            that more did. Adding it to the row count gives the window's true
            total. Non-zero means the list is the most recent slice, not the
            full window.
          default: 0
      type: object
      title: RecentlyStoppedSection
      description: >-
        Tests that finished in the lookback window, and what the list left out.


        A list rather than a bare count because the useful question is which

        channels came free and what was on them. It is capped for the same
        reason

        the other list sections are: ``recently_stopped_hours`` reaches back up
        to

        90 days, and a large lab on short protocols finishes thousands of runs
        in

        that time — too many to fetch, hold, or render as one table.
    StoppingSoonSection:
      properties:
        channels:
          items:
            $ref: '#/components/schemas/UpcomingStopRow'
          type: array
          title: Channels
          description: Expected releases, earliest first
        planned_beyond_cap:
          type: integer
          title: Planned Beyond Cap
          description: >-
            Scheduled releases in the window that the display cap excluded.
            Non-zero means the forecast is incomplete and the cap should rise.
          default: 0
        unassigned_count:
          type: integer
          title: Unassigned Count
          description: >-
            Scheduled releases with no channel assigned yet, counted among the
            plans the query returned. Not listed because the forecast is per
            channel, but real work that will free a channel once assigned — so
            not a truncation.


            Always 0 while the schema requires a channel on a scheduled plan;
            see the class docstring for why the field is kept.


            A lower bound when ``planned_beyond_cap`` is non-zero: plans past
            the cap were never inspected, so a channel-less one among them is
            counted there instead. Both figures stay individually true and both
            point at the same remedy — raise the cap — but do not read this as a
            window-wide total unless ``planned_beyond_cap`` is 0.
          default: 0
      type: object
      title: StoppingSoonSection
      description: >-
        Channels forecast to free up, and what the forecast could not place.


        A scheduled release can be missing from the list for two unrelated
        reasons,

        counted separately because they need different responses and would be

        uninterpretable summed together: the list hit its cap

        (``planned_beyond_cap``), or the plan has no channel assigned yet

        (``unassigned_count``) and so cannot be attributed to one.


        Running tests are bounded by the channel count and so are never capped.


        .. note::

            ``unassigned_count`` is **always 0 under the current schema**.
            ``planned_measurements_status_shape_check`` requires ``channel_id IS
            NOT NULL`` for a ``scheduled`` plan, so the state it counts cannot be
            stored. It is kept, rather than dropped as dead code, because the
            constraint is the only thing preventing it: booking work before
            assigning a channel is a coherent lab workflow, and were the status
            shape relaxed to allow it the field would start reporting without any
            other change. Read a non-zero value as a signal that the schema moved.
    PastDueSection:
      properties:
        measurements:
          items:
            $ref: '#/components/schemas/PastDueRow'
          type: array
          title: Measurements
          description: Overdue plans, most overdue first
        total:
          type: integer
          title: Total
          description: >-
            Every past-due plan, before the display limit. Greater than
            len(measurements) when the list was capped. Unlike the accumulated
            slip — which is just the sum of the rows' ``overdue_days`` and so is
            left to the reader — this cannot be derived from the listed rows.
      type: object
      required:
        - total
      title: PastDueSection
      description: >-
        Plans that should have started and have not.


        Split out of the queue rather than filtered into it. The queue answers
        "what

        is coming up", and a plan whose start time has passed is not upcoming
        work —

        counting it there both inflates the horizon total and hides the more
        urgent

        fact that something slipped. These need a decision (start it, reschedule
        it,

        cancel it) rather than a slot.
    QueueSection:
      properties:
        horizon_days:
          type: integer
          title: Horizon Days
          description: Only plans starting within this many days are listed
        measurements:
          items:
            $ref: '#/components/schemas/QueuedMeasurementRow'
          type: array
          title: Measurements
          description: Queued plans, earliest start first
        total_in_horizon:
          type: integer
          title: Total In Horizon
          description: >-
            Plans starting within the horizon, before the display limit is
            applied. Greater than len(measurements) when the list was capped.
        undated_count:
          type: integer
          title: Undated Count
          description: >-
            Requested plans with no planned start time. Not listed (they have no
            queue position) but surfaced so the backlog is visible.
      type: object
      required:
        - horizon_days
        - total_in_horizon
        - undated_count
      title: QueueSection
      description: >-
        The near-term queue, with enough context to trust what is missing.


        A queue is only meaningful if the reader knows what was left out, so the

        section carries its own horizon and totals rather than silently
        truncating:

        ``total_in_horizon`` is every plan starting inside the window (not just
        the

        listed ones) and ``undated_count`` is the backlog of requests with no
        date

        yet, which have no place in a time-ordered queue but should not vanish.


        Bounded on both sides: only plans starting between the report time and
        the

        horizon. A plan whose start time has already passed is not upcoming work
        and

        belongs in :class:`PastDueSection`.
    UnscheduledRequestsSection:
      properties:
        requests:
          items:
            $ref: '#/components/schemas/UnscheduledRequestRow'
          type: array
          title: Requests
          description: Unscheduled requests, longest-waiting first
        total:
          type: integer
          title: Total
          description: >-
            All unscheduled requests, before the display limit. Greater than
            len(requests) when the list was capped.
        total_estimated_hours:
          anyOf:
            - type: number
            - type: 'null'
          title: Total Estimated Hours
          description: >-
            Combined estimated duration of the listed requests, in hours — the
            channel-time the backlog will need. Null when no request carries an
            estimate.
      type: object
      required:
        - total
      title: UnscheduledRequestsSection
      description: >-
        Requested tests with no slot, longest-waiting first.


        Sorted oldest-request-first rather than newest: the point of the section
        is

        to surface what has been forgotten, and the request that has waited
        longest

        is the one most likely to have been.
    MaintenanceSection:
      properties:
        channels:
          items:
            $ref: '#/components/schemas/MaintenanceChannelRow'
          type: array
          title: Channels
          description: >-
            Channel-local faults awaiting repair, longest outage first. Channels
            down as part of an instrument-level visit are reported once in
            ``serviced_cyclers`` instead of once per channel here.
        serviced_cyclers:
          items:
            $ref: '#/components/schemas/ServicedCyclerRow'
          type: array
          title: Serviced Cyclers
          description: Whole cyclers out under one service event, longest first
        calibration:
          items:
            $ref: '#/components/schemas/CalibrationRow'
          type: array
          title: Calibration
          description: >-
            Cyclers on a calibration schedule that are overdue or due soon, most
            urgent first. Empty when everything on a schedule is current.
        scheduled_service:
          items:
            $ref: '#/components/schemas/ScheduledServiceRow'
          type: array
          title: Scheduled Service
          description: Booked visits inside the horizon, soonest first
        retired_count:
          type: integer
          title: Retired Count
          description: >-
            Out-of-service channels excluded as retirements — not awaiting
            anyone's attention, so listed nowhere above, but counted by the Lab
            wall's out-of-service total.
          default: 0
        recovery:
          anyOf:
            - $ref: '#/components/schemas/ChannelRecovery'
            - type: 'null'
          description: Outages closed and opened during the report's window
        recovered_delta:
          anyOf:
            - $ref: '#/components/schemas/PeriodDelta'
            - type: 'null'
          description: >-
            Outages closed this window against the previous one. A fall is not
            automatically bad — a lab with nothing broken repairs nothing — so
            read it alongside ``recovery.taken_out``.
        delta_window:
          anyOf:
            - $ref: '#/components/schemas/ComparisonWindow'
            - type: 'null'
          description: Which two windows ``recovered_delta`` compares
        service_changes:
          items:
            $ref: '#/components/schemas/ServiceChangePeriod'
          type: array
          title: Service Changes
          description: >-
            Repairs and removals over each lookback, shortest first. The
            incident lookback is derived from the same set of periods, so every
            entry here is backed by fetched history rather than defaulting to
            zero.
      type: object
      title: MaintenanceSection
      description: >-
        Equipment service: what is down, what is due, and what is booked.


        Carries the outage list, the repair counts, the calibration schedule,
        and

        upcoming visits, because they answer one question together — "are we on
        top

        of the equipment?" — and none is much use alone. Six down channels reads

        very differently next to "eight repaired this week" than next to "none",
        and

        a lab that is current on repairs can still be three months overdue on

        calibration.


        Outages are reported at the level the work happens at: a channel-local
        fault

        is a channel row, an instrument-level visit is one cycler row carrying
        its

        channel count. Without that split, one calibration on a 40-channel
        cycler

        would bury every genuine channel fault in the project.


        ``retired_count`` exists so the section can never silently disagree with
        the

        Lab wall: the wall counts every out-of-commission channel, this section
        lists

        only the ones a repair could return, and without the count the
        difference

        would read as a missing row.
    IdleChannelsSection:
      properties:
        tiers:
          items:
            $ref: '#/components/schemas/IdleTierCount'
          type: array
          title: Tiers
          description: Counts per threshold, ascending
        channels:
          items:
            $ref: '#/components/schemas/IdleChannelRow'
          type: array
          title: Channels
          description: Idle channels, longest-idle first
      type: object
      title: IdleChannelsSection
      description: >-
        Idle healthy channels, plus a count at each requested threshold.


        Rows are every channel past the *lowest* threshold, longest-idle first;
        the

        tier counts then say how many also cross the higher ones (the classic

        "N idle over 2 days, of which M over 3" phrasing) without listing a
        channel

        more than once.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
    DailyUtilizationPoint:
      properties:
        sampled_at:
          type: string
          format: date-time
          title: Sampled At
          description: Instant this point describes
        channels_in_use:
          type: integer
          title: Channels In Use
          description: Channels running a test then
        utilization_percent:
          type: number
          title: Utilization Percent
          description: channels_in_use over today's channel count
        channels_out_of_service:
          type: integer
          title: Channels Out Of Service
          description: Channels under an open outage at that instant
          default: 0
        utilization_of_available_percent:
          type: number
          title: Utilization Of Available Percent
          description: >-
            channels_in_use over the channels that were actually usable then —
            today's channel count less those out of service at that instant.
            Computable historically only because outages have spans.
          default: 0
      type: object
      required:
        - sampled_at
        - channels_in_use
        - utilization_percent
      title: DailyUtilizationPoint
      description: Lab utilization reconstructed at one instant in the trend window.
    PeriodDelta:
      properties:
        current:
          type: number
          title: Current
          description: Value over the current window
        previous:
          type: number
          title: Previous
          description: Value over the window immediately before it
        change:
          type: number
          title: Change
          description: current - previous
        change_percent:
          anyOf:
            - type: number
            - type: 'null'
          title: Change Percent
          description: Change as a percentage of previous; null when previous is 0
      type: object
      required:
        - current
        - previous
        - change
      title: PeriodDelta
      description: >-
        One output metric this period against the period immediately before it.


        ``change_percent`` is null when the previous period was zero: a rise
        from

        nothing is not a percentage, and rendering it as +100% (or as an
        infinity)

        would misstate it.
    ComparisonWindow:
      properties:
        days:
          type: integer
          title: Days
          description: Length of each window, in days
        current_start:
          type: string
          format: date-time
          title: Current Start
          description: Start of the window the metrics describe
        current_end:
          type: string
          format: date-time
          title: Current End
          description: End of that window — the report's own generation time
        previous_start:
          type: string
          format: date-time
          title: Previous Start
          description: >-
            Start of the window immediately before it. It ends where the current
            one starts, so the two are adjacent and non-overlapping.
      type: object
      required:
        - days
        - current_start
        - current_end
        - previous_start
      title: ComparisonWindow
      description: >-
        The two windows a period-over-period delta compares.


        Carried in the payload rather than left implicit so a reader — and an
        API

        consumer — never has to work out what "the previous period" was. A delta
        of

        +2 repairs means nothing until you know whether it is against last week
        or

        last month, and the report states it once per section instead of
        appending a

        vague suffix to every metric.
    UtilizationRow:
      properties:
        label:
          type: string
          title: Label
          description: Name of the group (lab, site, or cycler)
        total_channels:
          type: integer
          title: Total Channels
          description: Channels in this group
        occupied:
          type: integer
          title: Occupied
          description: Channels running a freshly-updated test
        stale:
          type: integer
          title: Stale
          description: Channels whose test has stopped updating
        free:
          type: integer
          title: Free
          description: Channels available for work
        out_of_commission:
          type: integer
          title: Out Of Commission
          description: Channels out of service
        utilization_percent:
          type: number
          title: Utilization Percent
          description: (occupied + stale) / total_channels, as a percentage
        utilization_of_available_percent:
          type: number
          title: Utilization Of Available Percent
          description: >-
            (occupied + stale) / (total_channels - out_of_commission), as a
            percentage. 0 when every channel is out of commission.
      type: object
      required:
        - label
        - total_channels
        - occupied
        - stale
        - free
        - out_of_commission
        - utilization_percent
        - utilization_of_available_percent
      title: UtilizationRow
      description: >-
        Channel-state counts and utilization for one grouping of channels.


        ``utilization_percent`` matches the Lab wall's headline figure: occupied

        plus stale over *all* channels. ``utilization_of_available_percent``

        excludes out-of-commission channels from the denominator, answering the

        different question "how hard is the usable capacity being used?" — the
        two

        diverge exactly when equipment is out of service, which is when the

        distinction matters most.
    UtilizationComparison:
      properties:
        label:
          type: string
          title: Label
          description: Human label for the lookback, e.g. '7 days'
        hours_ago:
          type: integer
          title: Hours Ago
          description: How far back this comparison reaches
        previous_utilization_percent:
          type: number
          title: Previous Utilization Percent
          description: Lab utilization at that instant
        change_points:
          type: number
          title: Change Points
          description: >-
            Change in lab utilization since then, in percentage points. Positive
            means the lab is busier now.
      type: object
      required:
        - label
        - hours_ago
        - previous_utilization_percent
        - change_points
      title: UtilizationComparison
      description: >-
        Headline utilization now against one point in the past.


        Several of these together separate a blip from a trend: a lab can be
        down

        against yesterday while still well up on last month, and acting on the

        first number alone would be a mistake.
    ProgramUtilizationRow:
      properties:
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program name; null for tests with no program assigned
        channels_in_use:
          type: integer
          title: Channels In Use
          description: Occupied or stale channels running this program
        percent_of_lab:
          type: number
          title: Percent Of Lab
          description: channels_in_use over all channels, as a percentage
        percent_of_in_use:
          type: number
          title: Percent Of In Use
          description: channels_in_use over all occupied+stale channels, as a percentage
      type: object
      required:
        - channels_in_use
        - percent_of_lab
        - percent_of_in_use
      title: ProgramUtilizationRow
      description: >-
        How much of the lab one test program is occupying right now.


        A program does not own channels, so there is no meaningful "total" to

        divide by; a program's footprint is expressed as a share of the lab and
        a

        share of the work currently in flight.
    StoppedMeasurementRow:
      properties:
        channel:
          $ref: '#/components/schemas/ChannelRef'
          description: Channel the test ran on
        measurement_id:
          type: string
          title: Measurement Id
          description: Measurement ID
        measurement_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Measurement Name
          description: Measurement name
        cell_instance_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Instance Name
          description: Cell instance tested
        cell_specification_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Specification Name
          description: Cell specification
        protocol_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Protocol Name
          description: Protocol that was run
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program the test belonged to
        start_time:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Start Time
          description: When the test started
        end_time:
          type: string
          format: date-time
          title: End Time
          description: When the test finished
        duration_hours:
          anyOf:
            - type: number
            - type: 'null'
          title: Duration Hours
          description: Run duration in hours, when a start time is recorded
      type: object
      required:
        - channel
        - measurement_id
        - end_time
      title: StoppedMeasurementRow
      description: A measurement that finished within the report's lookback window.
    UpcomingStopRow:
      properties:
        channel:
          $ref: '#/components/schemas/ChannelRef'
          description: Channel expected to free up
        source:
          $ref: '#/components/schemas/UpcomingStopSource'
          description: Whether this is a running test or a scheduled reservation
        reference_id:
          type: string
          title: Reference Id
          description: Measurement ID (running) or planned measurement ID (planned)
        name:
          anyOf:
            - type: string
            - type: 'null'
          title: Name
          description: Measurement or plan name
        cell_instance_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Instance Name
          description: Cell instance on test
        cell_specification_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Specification Name
          description: Cell specification
        protocol_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Protocol Name
          description: Protocol being run
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program the test belongs to
        expected_end_time:
          type: string
          format: date-time
          title: Expected End Time
          description: Forecast time the channel is released
        hours_until_end:
          type: number
          title: Hours Until End
          description: Hours from the report time to expected_end_time
      type: object
      required:
        - channel
        - source
        - reference_id
        - expected_end_time
        - hours_until_end
      title: UpcomingStopRow
      description: A channel expected to free up within the report's forecast window.
    PastDueRow:
      properties:
        planned_measurement_id:
          type: string
          title: Planned Measurement Id
          description: Planned measurement ID
        name:
          type: string
          title: Name
          description: Planned measurement name
        status:
          type: string
          title: Status
          description: '``requested`` or ``scheduled``'
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program this test belongs to
        cell_specification_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Specification Name
          description: Cell specification to be tested
        cell_instance_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Instance Name
          description: Cell instance assigned, if any
        protocol_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Protocol Name
          description: Protocol to run
        channel:
          anyOf:
            - $ref: '#/components/schemas/ChannelRef'
            - type: 'null'
          description: Reserved channel; null while the plan is unscheduled
        planned_start_time:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Planned Start Time
          description: When the run is planned to start
        planned_end_time:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Planned End Time
          description: When the run is planned to release the channel
        estimated_duration_seconds:
          anyOf:
            - type: integer
            - type: 'null'
          title: Estimated Duration Seconds
          description: Expected run duration, for plans with no times yet
        requested_by_email:
          anyOf:
            - type: string
            - type: 'null'
          title: Requested By Email
          description: Who requested the test
        overdue_days:
          type: number
          title: Overdue Days
          description: >-
            Days since ``planned_start_time`` passed, to one decimal. Always
            positive: a row is only past due once its start time is behind us.
      type: object
      required:
        - planned_measurement_id
        - name
        - status
        - overdue_days
      title: PastDueRow
      description: >-
        A plan whose start time has passed while it is still waiting to run.


        Extends the queue row rather than wrapping it: this is the same planned

        measurement, read with a different question in mind — not "what is
        coming

        up" but "what should already have happened". Inheriting keeps the field
        set

        flat, matching :class:`UnscheduledRequestRow`, which carries its own

        ``waiting_days`` the same way.
    QueuedMeasurementRow:
      properties:
        planned_measurement_id:
          type: string
          title: Planned Measurement Id
          description: Planned measurement ID
        name:
          type: string
          title: Name
          description: Planned measurement name
        status:
          type: string
          title: Status
          description: '``requested`` or ``scheduled``'
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program this test belongs to
        cell_specification_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Specification Name
          description: Cell specification to be tested
        cell_instance_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Instance Name
          description: Cell instance assigned, if any
        protocol_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Protocol Name
          description: Protocol to run
        channel:
          anyOf:
            - $ref: '#/components/schemas/ChannelRef'
            - type: 'null'
          description: Reserved channel; null while the plan is unscheduled
        planned_start_time:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Planned Start Time
          description: When the run is planned to start
        planned_end_time:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Planned End Time
          description: When the run is planned to release the channel
        estimated_duration_seconds:
          anyOf:
            - type: integer
            - type: 'null'
          title: Estimated Duration Seconds
          description: Expected run duration, for plans with no times yet
        requested_by_email:
          anyOf:
            - type: string
            - type: 'null'
          title: Requested By Email
          description: Who requested the test
      type: object
      required:
        - planned_measurement_id
        - name
        - status
      title: QueuedMeasurementRow
      description: >-
        A planned measurement waiting to run, with its program and cell.


        Shared by :class:`PastDueSection` and :class:`QueueSection`: both
        describe

        the same entity, one selecting plans whose start time has passed and the

        other plans still ahead.
    UnscheduledRequestRow:
      properties:
        planned_measurement_id:
          type: string
          title: Planned Measurement Id
          description: Planned measurement ID
        name:
          type: string
          title: Name
          description: Planned measurement name
        program_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Program Name
          description: Program this test belongs to
        cell_specification_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Specification Name
          description: Cell specification to be tested
        cell_instance_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cell Instance Name
          description: Cell instance assigned, if any
        protocol_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Protocol Name
          description: Protocol to run
        estimated_duration_seconds:
          anyOf:
            - type: integer
            - type: 'null'
          title: Estimated Duration Seconds
          description: Expected run duration, for capacity planning
        requested_by_email:
          anyOf:
            - type: string
            - type: 'null'
          title: Requested By Email
          description: Who asked for the test
        requested_at:
          type: string
          format: date-time
          title: Requested At
          description: When the request was raised
        waiting_days:
          type: number
          title: Waiting Days
          description: Days the request has been waiting for a slot
      type: object
      required:
        - planned_measurement_id
        - name
        - requested_at
        - waiting_days
      title: UnscheduledRequestRow
      description: >-
        A test someone asked for that has not been given a slot yet.


        These have no ``planned_start_time``, so they cannot appear in a

        time-ordered queue — which is exactly why they need their own section.
        An

        unscheduled request is invisible on the Lab wall and absent from the
        queue,

        so without this it is work that only the requester remembers.
    MaintenanceChannelRow:
      properties:
        channel:
          $ref: '#/components/schemas/ChannelRef'
          description: The out-of-service channel
        notes:
          anyOf:
            - type: string
            - type: 'null'
          title: Notes
          description: >-
            Why the channel is down: the incident's notes, falling back to the
            channel's own notes when there is no open incident.
        down_since:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Down Since
          description: >-
            When the outage began. Null when no open incident was found (see
            class docs).
        down_for_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Down For Days
          description: Days elapsed since ``down_since``, to one decimal
        category:
          anyOf:
            - $ref: '#/components/schemas/IncidentCategory'
            - type: 'null'
          description: Cause of the outage, from the open incident
        due_back:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Due Back
          description: >-
            When the outage's planned service was booked to end. Only planned
            visits carry one — an unplanned fault has no promised return, which
            is a different thing from a return that is merely unknown.
        overdue_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Overdue Days
          description: >-
            Days past ``due_back``, to one decimal, for a channel still down
            after its booked return. Null when inside the window or when no
            return was booked, on the same reasoning as the cycler-level row:
            zero would assert the channel is exactly on time.
        is_estimated:
          type: boolean
          title: Is Estimated
          description: >-
            ``down_since`` was reconstructed by the backfill from the channel's
            last-modified time rather than recorded when it happened.
          default: false
      type: object
      required:
        - channel
      title: MaintenanceChannelRow
      description: >-
        A channel currently marked out of commission.


        ``down_since`` is the open incident's ``started_at`` — a real outage
        start,

        not the channel row's update timestamp this once used as a proxy.


        A channel can be out of service with no open incident: it was flipped

        before ``channel_incidents`` existed and the backfill has not run in
        this

        environment yet. The outage fields are then all null, and the row still

        appears — a down channel must never disappear from the report because
        its

        history is missing.
    ServicedCyclerRow:
      properties:
        cycler_id:
          type: string
          title: Cycler Id
          description: The cycler being serviced
        cycler_name:
          type: string
          title: Cycler Name
          description: Cycler name
        site_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Site Name
          description: Site the cycler sits at
        event_type:
          $ref: '#/components/schemas/ServiceEventType'
          description: What kind of work this is
        channels_down:
          type: integer
          title: Channels Down
          description: Channels this event has taken out of service
        scheduled_for:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Scheduled For
          description: When the visit was booked
        scheduled_until:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Scheduled Until
          description: When the visit was booked to end, if an end was given
        down_since:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Down Since
          description: When the earliest of its channel outages opened
        down_for_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Down For Days
          description: Days elapsed since ``down_since``, to one decimal
        overdue_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Overdue Days
          description: >-
            Days past ``scheduled_until``, to one decimal, for a visit still
            open after its booked return. Null when the visit is inside its
            window or open-ended — an absent booked end is not lateness, and
            reporting it as zero would claim the visit is exactly on time.
        notes:
          anyOf:
            - type: string
            - type: 'null'
          title: Notes
          description: Free-text detail about the service
      type: object
      required:
        - cycler_id
        - cycler_name
        - event_type
        - channels_down
      title: ServicedCyclerRow
      description: >-
        A whole cycler out of service under one instrument-level event.


        Calibration and preventive maintenance are performed on the instrument,
        so

        a 40-channel cycler going in for its annual calibration is one visit —
        not

        forty unrelated channel faults that happen to share a category.
        Reporting it

        once, with the channel count, is what keeps a single calibration from

        burying every genuine channel-local fault under forty rows.
    CalibrationRow:
      properties:
        cycler_id:
          type: string
          title: Cycler Id
          description: Cycler ID
        cycler_name:
          type: string
          title: Cycler Name
          description: Cycler name
        site_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Site Name
          description: Site the cycler sits at
        channel_count:
          type: integer
          title: Channel Count
          description: Channels that go offline when this cycler is serviced
        last_calibrated_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Last Calibrated At
          description: When it was last calibrated; null if never
        calibration_due_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Calibration Due At
          description: When calibration next falls due
        days_until_due:
          anyOf:
            - type: number
            - type: 'null'
          title: Days Until Due
          description: >-
            Days until ``calibration_due_at``, to one decimal. Negative when
            already overdue. Null when the cycler has never been calibrated,
            which is not the same as being zero days from due.
        is_overdue:
          type: boolean
          title: Is Overdue
          description: '``calibration_due_at`` has passed'
          default: false
        never_calibrated:
          type: boolean
          title: Never Calibrated
          description: >-
            On a calibration schedule but with no recorded calibration, so no
            due date can be derived. Listed first: it is the least visible state
            and the one most likely to have been forgotten.
          default: false
      type: object
      required:
        - cycler_id
        - cycler_name
        - channel_count
      title: CalibrationRow
      description: >-
        A cycler's position against its calibration schedule.


        Only cyclers actually on a schedule appear: one with no

        ``calibration_interval_days`` has not opted in, and reporting it as
        "never

        calibrated" would be a finding about configuration, not about equipment.
    ScheduledServiceRow:
      properties:
        service_event_id:
          type: string
          title: Service Event Id
          description: Service event ID
        cycler_id:
          type: string
          title: Cycler Id
          description: The cycler booked in
        cycler_name:
          type: string
          title: Cycler Name
          description: Cycler name
        site_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Site Name
          description: Site the cycler sits at
        event_type:
          $ref: '#/components/schemas/ServiceEventType'
          description: What kind of work this is
        scheduled_for:
          type: string
          format: date-time
          title: Scheduled For
          description: When the visit is booked
        scheduled_until:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Scheduled Until
          description: >-
            When the visit is booked to end. Null on an open-ended booking,
            which is a real state and not a gap to fill in: a visit whose end
            nobody estimated must not be reported as ending the moment it
            starts.
        days_until:
          type: number
          title: Days Until
          description: Days from the report time to the visit, to one decimal
        duration_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Duration Days
          description: >-
            Booked length of the visit in days, to one decimal. Null when the
            booking is open-ended. This is how long the channels below are
            expected to be gone, which is what decides whether a visit is a blip
            or a plan the queue has to route around.
        channels_affected:
          type: integer
          title: Channels Affected
          description: >-
            Channels that will go offline. The capacity cost of the visit, and
            the reason a booking belongs in a report rather than only a diary.
        notes:
          anyOf:
            - type: string
            - type: 'null'
          title: Notes
          description: Free-text detail about the service
      type: object
      required:
        - service_event_id
        - cycler_id
        - cycler_name
        - event_type
        - scheduled_for
        - days_until
        - channels_affected
      title: ScheduledServiceRow
      description: A booked service visit that will take a cycler offline.
    ChannelRecovery:
      properties:
        recovered:
          type: integer
          title: Recovered
          description: Outages closed during the period — channels repaired
        taken_out:
          type: integer
          title: Taken Out
          description: Outages opened during the period — channels removed
        net:
          type: integer
          title: Net
          description: recovered - taken_out. Positive means capacity returning.
        mean_repair_hours:
          anyOf:
            - type: number
            - type: 'null'
          title: Mean Repair Hours
          description: >-
            Mean hours from outage start to resolution over the period's closed
            incidents, excluding rows whose start was estimated by the backfill.
            Null when nothing measurable was resolved.
        by_category:
          additionalProperties:
            type: integer
          type: object
          title: By Category
          description: Count of the period's closed outages by cause
      type: object
      required:
        - recovered
        - taken_out
        - net
      title: ChannelRecovery
      description: >-
        Channels repaired and taken out of service during the period.


        Counted from ``channel_incidents`` outage spans, so these are
        maintenance

        events rather than the capacity proxy this once carried: ``recovered``
        means

        an outage was closed, not that a channel happened to start running
        again.


        ``taken_out`` is deliberately not called ``dropped``. The proxy's
        "dropped"

        meant "stopped running anything", which a fully-booked lab produces all
        the

        time; this counts only a deliberate removal from service.
    ServiceChangePeriod:
      properties:
        label:
          type: string
          title: Label
          description: Human label for the lookback, e.g. '7 days'
        hours_ago:
          type: integer
          title: Hours Ago
          description: How far back this period reaches
        recovered:
          type: integer
          title: Recovered
          description: Outages closed within the period
        taken_out:
          type: integer
          title: Taken Out
          description: Outages opened within the period
        net:
          type: integer
          title: Net
          description: recovered - taken_out
        mean_repair_hours:
          anyOf:
            - type: number
            - type: 'null'
          title: Mean Repair Hours
          description: Mean repair time over the period's measurable repairs
      type: object
      required:
        - label
        - hours_ago
        - recovered
        - taken_out
        - net
      title: ServiceChangePeriod
      description: >-
        Repairs and removals over one lookback, for the multi-period ladder.


        The trend window alone cannot separate a bad week from a bad quarter:
        three

        channels repaired in the last 24 hours reads very differently against
        two in

        the last 30 days than against forty. Uses the same lookbacks as

        :class:`UtilizationComparison` so the two ladders line up.
    IdleTierCount:
      properties:
        threshold_days:
          type: integer
          title: Threshold Days
          description: Idle threshold in days
        channel_count:
          type: integer
          title: Channel Count
          description: Channels idle for at least this many days
      type: object
      required:
        - threshold_days
        - channel_count
      title: IdleTierCount
      description: How many idle channels cross one reporting threshold.
    IdleChannelRow:
      properties:
        channel:
          $ref: '#/components/schemas/ChannelRef'
          description: The idle channel
        last_used_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Last Used At
          description: >-
            When this channel's most recent test finished. Null when no finished
            test was found within the service's lookback.
        idle_for_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Idle For Days
          description: Days since last_used_at; null when never used
        max_amps:
          anyOf:
            - type: number
            - type: 'null'
          title: Max Amps
          description: Maximum rated current (A)
        min_volts:
          anyOf:
            - type: number
            - type: 'null'
          title: Min Volts
          description: Minimum rated voltage (V)
        max_volts:
          anyOf:
            - type: number
            - type: 'null'
          title: Max Volts
          description: Maximum rated voltage (V)
      type: object
      required:
        - channel
      title: IdleChannelRow
      description: A healthy, free channel that has not run anything for a long time.
    ChannelRef:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Channel ID
        channel_name:
          type: string
          title: Channel Name
          description: Channel name
        cycler_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Cycler Id
          description: Owning cycler ID
        cycler_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Cycler Name
          description: Owning cycler name
        site_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Site Name
          description: Site the cycler sits at
      type: object
      required:
        - channel_id
        - channel_name
      title: ChannelRef
      description: Where a channel lives, denormalized so a report row stands alone.
    UpcomingStopSource:
      type: string
      enum:
        - running
        - planned
      title: UpcomingStopSource
      description: Where an upcoming channel release was forecast from.
    IncidentCategory:
      type: string
      enum:
        - hardware_failure
        - maintenance
        - calibration
        - decommissioned
        - other
      title: IncidentCategory
      description: >-
        Why a channel is out of service.


        Mirrors the ``channel_incidents_shape_check`` CHECK constraint. The
        database

        stores this as CHECK-constrained text rather than a Postgres enum so the

        allowed set can be widened by swapping a constraint — ``ALTER TYPE ...
        ADD

        VALUE`` does not compose with the additive-migration rule.


        ``decommissioned`` is a retirement, not a repair: a channel in this
        state is

        never expected back, so its incident stays open forever. The report
        layer must

        exclude it both from mean-time-to-repair (there is no repair to time)
        and from

        "channels currently down" (it is not awaiting anyone's attention). The

        frontend already encodes that rule; the report-side constant lands with
        the

        report that consumes it.
    ServiceEventType:
      type: string
      enum:
        - calibration
        - preventive_maintenance
        - firmware
        - repair
        - other
      title: ServiceEventType
      description: >-
        What kind of work took (or will take) a cycler out of service.


        Mirrors the ``cycler_service_events_shape_check`` CHECK constraint.
        Stored

        as CHECK-constrained text rather than a Postgres enum so the allowed set
        can

        be widened by swapping a constraint — ``ALTER TYPE ... ADD VALUE`` does
        not

        compose with the additive-migration rule.


        ``calibration`` is the one that feeds ``cyclers.last_calibrated_at``:

        completing an event of this type is what advances the calibration clock.

````