openapi: '3.1.0'
info:
    title: deadair
    version: main
    description: "The HTTP API of a deadair station. Every station is self-hosted, so the address is your own: the API is served under /api on the station's one published port. This document follows the main branch."
servers:
    - url: 'http://localhost:8080/api'
      description: 'A station on its default port. Substitute your own address.'
paths:
    '/activity':
        get:
            operationId: readActivity
            summary: 'Read activity'
            description: 'The feed, newest first, one page at a time'
            parameters:
                - name: ActivityQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ActivityQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ActivityPage'
    '/art/{id}':
        get:
            operationId: getArt
            summary: 'Get art'
            description: 'The bytes of one cached image'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'image/jpeg':
                            schema:
                                type: string
                                format: binary
                        'image/png':
                            schema:
                                type: string
                                format: binary
                        'image/webp':
                            schema:
                                type: string
                                format: binary
                        'image/gif':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/auth/token':
        post:
            operationId: requestToken
            summary: 'Request token'
            description: 'Request authenticated token'
            requestBody:
                required: true
                content:
                    'application/x-www-form-urlencoded':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationRequest'
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationRequest'
            security: []
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationTokenResponse'
    '/auth/login/register':
        post:
            operationId: registerLogin
            summary: 'Register login'
            description: 'Register a new login'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationRegistration'
            security: []
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationRegistration'
    '/auth/login/verify':
        post:
            operationId: verifyLoginRegistration
            summary: 'Verify login registration'
            description: 'Verify a login registration'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationRegistrationVerification'
            security: []
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationToken'
    '/auth/login/start':
        post:
            operationId: startLogin
            summary: 'Start login'
            description: 'Start a password-less login process'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationLoginStart'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationLoginStartResponse'
    '/auth/factors':
        get:
            operationId: listFactors
            summary: 'List factors'
            description: 'List authentication factors'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                type: array
                                items:
                                    '$ref': '#/components/schemas/AuthenticationFactor'
    '/auth/factors/register':
        post:
            operationId: registerFactor
            summary: 'Register factor'
            description: 'Register an authentication factor'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationFactorRegistration'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationFactorRegistrationResponse'
    '/auth/factors/verify':
        post:
            operationId: verifyFactorRegistration
            summary: 'Verify factor registration'
            description: 'Verify an authentication factor registration'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AuthenticationFactorRegistrationVerification'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthenticationToken'
    '/auth/factors/start':
        post:
            operationId: startFactorChallenge
            summary: 'Start factor challenge'
            description: 'Issue a factor verification challenge for a pending MFA round. Authenticated via the short-lived `mfa_challenge_id` in the body, not by session — this is the only /auth/factors/* route that does not require an authenticated session.'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/FactorChallengeStartRequest'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/FactorChallengeStartResponse'
    '/auth/mfa/start':
        post:
            operationId: startStepUpChallenge
            summary: 'Start MFA challenge'
            description: 'Mint a fresh MFA challenge for the *current* authenticated session so the SPA can satisfy a `step_up_required` denial. Optionally filters eligible factors against an inbound `StepUpRequirement` hint. Returns `enrollment_required` when no enrolled factor matches the requirement so the SPA can route the user into enrollment instead of getting stuck.'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/StepUpStartRequest'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StepUpStartResponse'
    '/auth/factors/{method}/{methodId}':
        delete:
            operationId: removeFactor
            summary: 'Remove factor'
            description: "Remove one of the caller's own factors. Answered only for `authenticator` today, and only after a recent strong-factor verification: the same gate enrolment sits behind once a strong factor exists, so a stolen session cannot quietly switch the second factor off. Removing the last authenticator turns the sign-in challenge off for that account."
            parameters:
                - name: method
                  in: path
                  required: true
                  schema:
                      '$ref': '#/components/schemas/AuthenticationFactorMethod'
                - name: methodId
                  in: path
                  required: true
                  schema:
                      type: string
                      maxLength: 255
            responses:
                '204':
                    description: 'No content'
    '/auth/logout':
        post:
            operationId: revokeCurrentSession
            summary: Logout
            description: "revoke the caller's current session (self sign-out). Deliberately carries no policy gate: signing out must always clear the browser's httpOnly refresh cookie, including for a caller whose access token has already expired. A 401 here would leave a 30-day refresh cookie behind that silently signs the user back in on the next page load. SessionsService revokes the session only when the caller is actually authenticated; an anonymous caller still gets 204 and a cleared cookie."
            security: []
            responses:
                '204':
                    description: 'No content'
    '/auth/session':
        get:
            operationId: readCurrentSession
            summary: 'Read session'
            description: 'Who the caller is and which platform roles they hold'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AuthSession'
    '/catalog/artists':
        get:
            operationId: listArtists
            summary: 'List artists'
            description: 'Every artist the station has ingested, ordered by name'
            parameters:
                - name: CatalogQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/CatalogQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ArtistPage'
    '/catalog/artists/{id}':
        get:
            operationId: getArtist
            summary: 'Get artist'
            description: 'One artist. 404s on an id that was merged away, since reads never return merged rows'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Artist'
    '/catalog/artists/{id}/enrichment':
        get:
            operationId: getArtistEnrichment
            summary: 'Get artist enrichment'
            description: 'What every enrichment provider said about this artist, and when each of them said it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ArtistEnrichmentDetail'
    '/catalog/artists/{id}/albums':
        get:
            operationId: listAlbumsByArtist
            summary: 'List artist albums'
            description: 'The albums credited to one artist'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
                - name: CatalogQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/CatalogQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AlbumPage'
    '/catalog/artists/{id}/rating':
        put:
            operationId: rateArtist
            summary: 'Rate artist'
            description: 'What the station thinks of this artist. A dislike here excludes every record they are credited on'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/RateInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Artist'
    '/catalog/albums':
        get:
            operationId: listAlbums
            summary: 'List albums'
            parameters:
                - name: CatalogQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/CatalogQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AlbumPage'
    '/catalog/albums/{id}':
        get:
            operationId: getAlbum
            summary: 'Get album'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Album'
    '/catalog/albums/{id}/enrichment':
        get:
            operationId: getAlbumEnrichment
            summary: 'Get album enrichment'
            description: "The record's own enrichment: the label, pressing and cover belong to the release, not to a track on it"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/AlbumEnrichmentDetail'
    '/catalog/albums/{id}/tracks':
        get:
            operationId: listTracksByAlbum
            summary: 'List album tracks'
            description: "One album's tracks"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
                - name: TrackQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/TrackQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackPage'
    '/catalog/albums/{id}/rating':
        put:
            operationId: rateAlbum
            summary: 'Rate album'
            description: 'What the station thinks of this record. A dislike here excludes every track on it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/RateInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Album'
    '/catalog/tracks/{id}':
        get:
            operationId: getTrack
            summary: 'Get track'
            description: 'One record and everything it has accumulated: its copies, its bytes, its measurement, what it has aired'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackDetail'
    '/catalog/tracks/{id}/audio':
        delete:
            operationId: clearAudio
            summary: 'Clear track audio'
            description: "Drop the station's own copies of this record. The next play fetches them again"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackClearResult'
                '409':
                    description: Conflict
    '/catalog/tracks/{id}/analysis':
        delete:
            operationId: clearAnalysis
            summary: 'Clear track analysis'
            description: 'Forget the measurement, so the walk takes it again'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackClearResult'
    '/catalog/tracks/{id}/retry':
        post:
            operationId: retryAudio
            summary: 'Retry track audio'
            description: "Try this record's copies again now, rather than when the backoff says"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackClearResult'
    '/catalog/tracks/{id}/offer':
        post:
            operationId: offerAudio
            summary: 'Offer track copies again'
            description: 'Put copies a provider refused back on offer, and clear their backoff so they are tried now'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackClearResult'
    '/catalog/tracks/{id}/enrichment':
        get:
            operationId: getTrackEnrichment
            summary: 'Get track enrichment'
            description: 'What the providers said about one recording, including everything no canonical column holds'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackEnrichmentDetail'
        delete:
            operationId: clearEnrichment
            summary: 'Clear track enrichment'
            description: 'Forget what the providers said, so the enrichment pass asks again'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
                - name: ClearEnrichmentQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ClearEnrichmentQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackClearResult'
    '/catalog/tracks':
        get:
            operationId: listTracks
            summary: 'List tracks'
            description: 'Every track, flat. The only way to answer "do we have this song?" without knowing its artist'
            parameters:
                - name: TrackQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/TrackQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TrackPage'
    '/catalog/tracks/{id}/rating':
        put:
            operationId: rateTrack
            summary: 'Rate track'
            description: 'What the station thinks of this song, which is the narrowest thing an opinion can be about'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/RateInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Track'
    '/charts':
        get:
            operationId: readCharts
            summary: 'List charts'
            description: 'Every chart every installed chart plugin currently offers'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationChartList'
    '/charts/{id}':
        get:
            operationId: readChart
            summary: 'Read chart'
            description: "One chart's records, ranked"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 400
                - name: ChartQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ChartQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ChartPage'
    '/clock/bands':
        get:
            operationId: list
            summary: 'List clock bands'
            description: "Every band on this station's clock, including the ones switched off, in the operator's own order"
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ClockBandList'
        post:
            operationId: create
            summary: 'Create clock band'
            description: 'Adds a band. It claims its first boundary on the next commit pass'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ClockBand'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ClockBandList'
    '/clock/bands/{id}':
        put:
            operationId: update
            summary: 'Update clock band'
            description: 'Rewrites one band. Breaks it has already planted stay where they are: the running order is the memory'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ClockBand'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ClockBandList'
        delete:
            operationId: remove
            summary: 'Delete clock band'
            description: 'Removes a band, which costs it the boundaries it had not claimed yet and nothing else'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ClockBandList'
    '/director/air':
        get:
            operationId: getAir
            summary: 'Get station air'
            description: 'What the station is airing, and whether it is driving at all'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAir'
        post:
            operationId: putOnAir
            summary: 'Put the station on air'
            description: 'Puts the station on air, building the running order from a playlist read at this moment. What is playing finishes: changing the programming is not a reason to cut a listener off mid-track'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PutOnAirInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAir'
        patch:
            operationId: setAirMode
            summary: 'Set the air mode'
            description: 'Changes what puts the station on air: only while somebody is listening, or whenever there is a programme. Takes effect at once rather than at the next boundary'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/SetStationAirInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAir'
    '/director/air/order':
        get:
            operationId: getOrder
            summary: 'Get the running order'
            description: 'The live running order, item by item, each saying where it has got to'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/director/air/persona':
        put:
            operationId: recast
            summary: 'Recast the broadcast'
            description: 'Changes who is presenting this broadcast. Breaks already written for it in the outgoing character are written again in the new one'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/SetStationHostInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/director/air/extend':
        post:
            operationId: extendOrder
            summary: 'Extend the running order'
            description: 'Queues a refill and returns at once. Generating a set walks the catalog, and an operator pressing a button should not be held open through it'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ExtendStationInput'
            responses:
                '202':
                    description: 'Response 202'
    '/director/air/replan':
        post:
            operationId: replanOrder
            summary: 'Replan the running order'
            description: 'Queues a fresh set for everything the player is not already holding, and swaps it in once it exists. The old tail keeps playing until then, because emptying the running order first would take the station off air while the model was still choosing'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ReplanStationInput'
            responses:
                '202':
                    description: 'Response 202'
    '/director/air/hold':
        patch:
            operationId: holdAgainstSchedule
            summary: 'Hold the station against the schedule'
            description: 'Holds the running order against the schedule, so a block boundary does not take back what an operator put on. A takeover is otherwise stamped with whichever slot was in force and is replaced when that block ends, which is correct and gives nobody any warning'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/HoldStationInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAir'
        delete:
            operationId: releaseToSchedule
            summary: 'Release the station to the schedule'
            description: 'Releases a hold, so the next block boundary changes the station over as it ordinarily would. A station with no hold is unchanged rather than refused'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAir'
    '/director/air/shuffle':
        post:
            operationId: shuffleOrder
            summary: 'Shuffle the running order'
            description: "Shuffles the records not yet handed to the player, and plants the breaks again around the new sequence. The head is already in the player's hands and is left alone"
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/director/air/segments':
        post:
            operationId: addSegmentToOrder
            summary: 'Add a segment to the running order'
            description: 'Puts something the station says into the running order. A segment with no audio yet is refused here rather than accepted and skipped when it comes round, so an operator is told why it cannot play'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AddStationSegmentInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/director/air/tracks':
        post:
            operationId: addTrackToOrder
            summary: 'Add a record to the running order'
            description: 'Puts a catalog record into the running order. A record whose audio is not local yet is refused here rather than accepted and held or skipped when its slot comes round, so an operator asking for a specific one is told why it cannot play. What makes this worth having on its own is undo: dropping an item only ever marks a segment, but a track is spliced out of the order entirely, so nothing could put one back until this existed'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/AddStationTrackInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/director/air/items/{itemId}':
        patch:
            operationId: moveOrderItem
            summary: 'Move a running order item'
            description: 'Moves an item. A position already handed to the player is refused rather than clamped'
            parameters:
                - name: itemId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/MoveStationItemInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
        delete:
            operationId: removeOrderItem
            summary: 'Remove a running order item'
            description: 'Drops an item that has not been handed to the player yet'
            parameters:
                - name: itemId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationOrder'
    '/history':
        get:
            operationId: readHistory
            summary: 'Read history'
            description: 'What the station played, newest first, one page at a time'
            parameters:
                - name: HistoryQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/HistoryQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/HistoryPage'
    '/news/feeds':
        get:
            operationId: readFeeds
            summary: 'List feeds'
            description: 'Every feed every installed news plugin currently offers'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationFeedList'
    '/news':
        get:
            operationId: readNews
            summary: 'Read news'
            description: 'Published entries, newest first'
            parameters:
                - name: NewsQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/NewsQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/NewsPage'
    '/nowplaying':
        get:
            operationId: getNowPlaying
            summary: 'Get now playing'
            description: 'What is on air right now. Answers 200 with `onAir: false` when the station is quiet, so a device polling this treats silence as an answer rather than an error'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/NowPlaying'
    '/onboarding':
        get:
            operationId: getOnboardingRequirements
            summary: 'Get Onboarding Requirements'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                type: array
                                items:
                                    '$ref': '#/components/schemas/OnboardingRequirement'
        post:
            operationId: submitOnboardingRequirement
            summary: 'Submit Onboarding Requirement'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/OnboardingRequirement'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                type: array
                                items:
                                    '$ref': '#/components/schemas/OnboardingRequirement'
    '/personas/{id}/auditions':
        get:
            operationId: list
            summary: 'List persona auditions'
            description: 'Every audition of this character, newest first, without their breaks'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaAuditionList'
        post:
            operationId: start
            summary: 'Start persona audition'
            description: 'Asks the station to put this character through a playlist. It is queued, not written'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaAuditionRequest'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaAudition'
    '/personas/{id}/auditions/{auditionId}':
        get:
            operationId: get
            summary: 'Get persona audition'
            description: 'One audition with every break it has written so far, in order'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: auditionId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaAudition'
    '/personas/{id}/auditions/{auditionId}/cancel':
        post:
            operationId: cancel
            summary: 'Cancel persona audition'
            description: 'Stops an audition where it stands, keeping the breaks it has already written'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: auditionId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaAuditionSummary'
    '/personas':
        get:
            operationId: list
            summary: 'List personas'
            description: 'Every persona this station has, oldest first'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
        post:
            operationId: create
            summary: 'Create persona'
            description: 'Writes a new persona. It is not put on air by creating it'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/Persona'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
    '/personas/generate':
        post:
            operationId: generate
            summary: 'Generate persona'
            description: 'Turns a description of a character into a whole persona, checked against its own sample lines and handed back unsaved'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaRequest'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/GeneratedPersona'
    '/personas/restore':
        post:
            operationId: restore
            summary: 'Restore station personas'
            description: "Writes back whichever of the station's own personas this station is missing, touching nothing it already has and putting nothing on air"
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
    '/personas/export':
        get:
            operationId: exportPersonas
            summary: 'Export personas'
            description: 'Every character this station holds, as one file'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaFile'
                    headers:
                        Content-Disposition:
                            schema:
                                type: string
    '/personas/{id}/export':
        get:
            operationId: exportPersona
            summary: 'Export persona'
            description: 'One character, its sheet and its stories, as a file'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaFile'
                    headers:
                        Content-Disposition:
                            schema:
                                type: string
                '404':
                    description: 'Not found'
    '/personas/import/preview':
        post:
            operationId: preview
            summary: 'Preview persona import'
            description: 'Reads a file and reports what importing it would create, rewrite and skip. Writes nothing'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaFile'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaImportPlan'
    '/personas/import':
        post:
            operationId: import
            summary: 'Import personas'
            description: 'Writes a file into this station, merging by key, and answers with what it did'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaFile'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaImportResult'
    '/personas/{id}':
        put:
            operationId: update
            summary: 'Update persona'
            description: 'Rewrites one persona. An edit to the one on air is heard on the next break'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/Persona'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
        delete:
            operationId: remove
            summary: 'Delete persona'
            description: 'Removes a persona, including the one on air, which leaves the station with none'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
    '/personas/{id}/active':
        put:
            operationId: setActive
            summary: 'Put persona on air'
            description: 'Puts this persona on air and takes the previous one off'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaList'
    '/personas/{id}/notes':
        get:
            operationId: list
            summary: 'List persona notes'
            description: 'Everything this character has accumulated, oldest first, in every state'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaNoteList'
        post:
            operationId: create
            summary: 'Write persona note'
            description: "Writes a note by hand. An operator's own note is active from the moment it exists; only the distil pass proposes"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaNoteWrite'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaNoteList'
    '/personas/{id}/notes/{noteId}':
        put:
            operationId: update
            summary: 'Update persona note'
            description: "Rewrites one note's words, whoever wrote it. Editing what the station proposed is most of the point of the panel"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: noteId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaNoteWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaNoteList'
        delete:
            operationId: remove
            summary: 'Delete persona note'
            description: 'Removes a note outright. Turning down a PROPOSAL is a state rather than this, or the next pass writes it again'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: noteId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaNoteList'
    '/personas/{id}/notes/{noteId}/state':
        put:
            operationId: setState
            summary: 'Set persona note state'
            description: "Accepts a proposal, turns one down, or rests an active note. Mirrors the lexicon's own state route"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: noteId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaNoteState'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaNoteList'
    '/personas/{id}/stories':
        get:
            operationId: list
            summary: 'List persona stories'
            description: 'Every story this character holds, oldest first, in every state'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
        post:
            operationId: create
            summary: 'Write persona story'
            description: "Writes a story by hand. An operator's own is tellable from the moment it exists; only the enrichment pass proposes"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryWrite'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/stories/{storyId}':
        put:
            operationId: update
            summary: 'Update persona story'
            description: "Rewrites one story's handle and telling, whoever wrote it"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
        delete:
            operationId: remove
            summary: 'Delete persona story'
            description: 'Removes a story outright, details and all. Turning down a PROPOSAL is a state rather than this, or the next pass writes it again'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/stories/{storyId}/state':
        put:
            operationId: setState
            summary: 'Set persona story state'
            description: 'Accepts a proposal, turns one down, or takes a story out of the rotation without losing it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryState'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/stories/{storyId}/details':
        post:
            operationId: addDetail
            summary: 'Add persona story detail'
            description: 'Adds one thing to a story that already exists'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryDetailWrite'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/stories/{storyId}/details/{detailId}':
        put:
            operationId: updateDetail
            summary: 'Update persona story detail'
            description: "Rewrites one detail's words"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: detailId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryDetailWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
        delete:
            operationId: removeDetail
            summary: 'Delete persona story detail'
            description: 'Removes one detail, leaving the story it was hung on alone'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: detailId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/stories/{storyId}/details/{detailId}/state':
        put:
            operationId: setDetailState
            summary: 'Set persona story detail state'
            description: 'Accepts a proposed detail or turns it down, which has to outlive the pass that proposed it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: storyId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
                - name: detailId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PersonaStoryState'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaStoryList'
    '/personas/{id}/rehearse':
        post:
            operationId: rehearse
            summary: 'Rehearse persona'
            description: 'Writes a talk break under this persona against two fixed invented records, and answers with every writer that was asked'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PersonaRehearsal'
    '/playlists':
        get:
            operationId: listPlaylists
            summary: 'List importable playlists'
            description: 'Fans out across every installed plugin that declares AND implements the `catalog` capability'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/CatalogPlaylistPage'
    '/playlists/{pluginId}/{playlistId}/tracks':
        get:
            operationId: getPlaylistTracks
            summary: 'Get playlist tracks'
            description: "One playlist's tracks from one plugin"
            parameters:
                - name: pluginId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
                - name: playlistId
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 400
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/CatalogPlaylistTracks'
    '/playout/status':
        get:
            operationId: getStatus
            summary: 'Get playout status'
            description: 'What the station is playing and what is queued behind it. The console polls this'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/playout/playlist':
        post:
            operationId: playPlaylist
            summary: 'Play a playlist'
            description: 'Loads a plugin playlist into the running order and starts handing it to the player. Replaces whatever was queued; what is on air finishes rather than being cut off'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PlayoutPlaylistInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/playout/chart':
        post:
            operationId: playChart
            summary: 'Play a chart'
            description: 'Builds the running order from a published chart and starts handing it to the player. The same replacement a playlist makes, from a document somebody else ranked'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PlayoutChartInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/playout/skip':
        post:
            operationId: skip
            summary: 'Skip the current item'
            description: 'Ends the item on air so the next one starts immediately. The station owns the decoder, so this lands at once rather than waiting out audio already committed to a player'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/playout/start':
        post:
            operationId: start
            summary: 'Start playout'
            description: 'Puts the station back on air with the running order it already has, picking it up where Stop left it. Distinct from putting a playlist on air, which builds a new broadcast and throws away what was there. Refused when there is nothing left to resume'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/playout/stop':
        post:
            operationId: stop
            summary: 'Stop playout'
            description: 'Stands the station down: stops what is on air at once and hands the mount back. The running order is LEFT as it is, so `/playout/start` can pick it up where this stopped it. deadair holds the mount on a lease it renews while it has something to play, so stopping goes quiet rather than falling through to a bed nobody programmed'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PlayoutStatus'
    '/plugins':
        get:
            operationId: listPlugins
            summary: 'List plugins'
            description: 'Lists every plugin the host knows about'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                type: array
                                items:
                                    '$ref': '#/components/schemas/PluginSummary'
    '/plugins/grants':
        get:
            operationId: listGrants
            summary: 'List plugin grants'
            description: 'Every capability an installed plugin is asking the operator for, with the answer so far'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginGrantList'
    '/plugins/rescan':
        post:
            operationId: rescanPlugins
            summary: 'Rescan plugins'
            description: 'Rescans the mounted plugin directory: registers new plugins, unloads removed ones'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                type: array
                                items:
                                    '$ref': '#/components/schemas/PluginSummary'
    '/plugins/{id}':
        get:
            operationId: getPlugin
            summary: 'Get plugin'
            description: 'One plugin, including its stored non-secret configuration and last error'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/config':
        put:
            operationId: updatePluginConfig
            summary: 'Update plugin configuration'
            description: "Validates against the plugin's own config schema, encrypts secrets, persists, and reinitializes"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PluginConfigInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/enable':
        post:
            operationId: enablePlugin
            summary: 'Enable plugin'
            description: 'Enables a plugin without resubmitting its configuration'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/disable':
        post:
            operationId: disablePlugin
            summary: 'Disable plugin'
            description: 'Disables a plugin and tears its instance down, keeping its configuration'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/grants':
        put:
            operationId: decideGrant
            summary: 'Decide plugin grant'
            description: 'Answers one capability this plugin asked for. Takes effect on the next fetch, with no reload'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PluginGrantInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginGrantList'
    '/plugins/{id}/reload':
        post:
            operationId: reloadPlugin
            summary: 'Reload plugin'
            description: "Reapplies the plugin's stored configuration: disposes the running instance and initializes it again"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/test':
        post:
            operationId: testPlugin
            summary: 'Test plugin connection'
            description: "Runs the plugin's own `testConnection()` through the invoker"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginTestResult'
    '/plugins/{id}/config/suggestions':
        post:
            operationId: suggestPluginConfigOptions
            summary: 'Suggest plugin config options'
            description: 'Asks the plugin what to offer for its config fields right now, through the invoker'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginFieldSuggestions'
    '/plugins/{id}/logs':
        get:
            operationId: getPluginLogs
            summary: 'Get plugin logs'
            description: "Returns the plugin's buffered log lines at or above the current log level"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
                - name: PluginLogQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/PluginLogQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginLogPage'
    '/plugins/{id}/logs/download':
        get:
            operationId: downloadPluginLogs
            summary: 'Download plugin logs'
            description: "Streams the plugin's full retained log as a plain-text attachment"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'text/plain':
                            schema:
                                type: string
                    headers:
                        Content-Disposition:
                            schema:
                                type: string
    '/plugins/{id}/logs/level':
        put:
            operationId: setPluginLogLevel
            summary: 'Set plugin log level'
            description: "Sets the minimum severity the plugin's log store retains going forward"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PluginLogLevelInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/oauth/authorize':
        get:
            operationId: startOAuthAuthorization
            summary: 'Start plugin OAuth authorization'
            description: "Reports where to send the operator for the provider's consent screen"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginOAuthStart'
    '/plugins/{id}/oauth':
        delete:
            operationId: disconnectOAuth
            summary: 'Disconnect plugin OAuth'
            description: "Forgets the plugin's stored OAuth tokens and reinitializes it"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginDetail'
    '/plugins/{id}/oauth/callback':
        get:
            operationId: completeOAuthCallback
            summary: 'Complete plugin OAuth authorization'
            description: 'Completes the flow. Anonymous: the provider redirects the browser here with no session of ours'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
                - name: PluginOAuthCallbackQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/PluginOAuthCallbackQuery'
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PluginOAuthResult'
    '/productions':
        get:
            operationId: list
            summary: 'List productions'
            description: 'Everything the station has made or is making, newest first'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ProductionList'
        post:
            operationId: request
            summary: 'Request production'
            description: 'Asks the station to make one. It is queued, not started'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ProductionRequest'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Production'
    '/productions/{id}/cancel':
        post:
            operationId: cancel
            summary: 'Cancel production'
            description: 'Stops a production being made, for good'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Production'
    '/segments':
        get:
            operationId: listSegments
            summary: 'List segments'
            description: 'Everything the station can play that is not a record'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/SegmentList'
        post:
            operationId: createSegment
            summary: 'Create segment'
            description: 'Plans something for the station to say, and starts rendering it'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/SegmentCreate'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Segment'
    '/segments/upload':
        post:
            operationId: uploadSegment
            summary: 'Upload segment'
            description: 'Takes a recording in from the browser and puts it in the library, ready to air'
            requestBody:
                required: true
                content:
                    'multipart/form-data':
                        schema:
                            '$ref': '#/components/schemas/SegmentUpload'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/Segment'
                '400':
                    description: 'Bad request'
                '413':
                    description: 'Response 413'
                '415':
                    description: 'Response 415'
    '/segments/scan':
        post:
            operationId: scanLibrary
            summary: 'Scan the segment inbox'
            description: 'Takes whatever audio is sitting in the inbox directory into the library. Safe to repeat: a segment is identified by its audio, so the same recording arriving twice is one segment'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/SegmentScanResult'
    '/scripts':
        get:
            operationId: readScriptHistory
            summary: 'Read script history'
            description: 'What the station has written lately, newest first, one page at a time'
            parameters:
                - name: ScriptHistoryQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ScriptHistoryQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScriptHistoryPage'
    '/scripts/{id}/rating':
        put:
            operationId: rateScript
            summary: 'Rate script'
            description: 'What the operator thought of this attempt. Nothing acts on it automatically'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ScriptRatingInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScriptAttempt'
    '/scripts/summary':
        get:
            operationId: readScriptSummary
            summary: 'Read script summary'
            description: 'Write attempts by outcome, per presenter, over a recent window'
            parameters:
                - name: ScriptHistorySummaryQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ScriptHistorySummaryQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScriptHistorySummary'
    '/voices':
        get:
            operationId: listVoices
            summary: 'List voices'
            description: 'The voices the station can be asked to speak in'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/VoiceList'
    '/voices/sample':
        get:
            operationId: getDefaultVoiceSample
            summary: 'Get default voice sample'
            description: 'A short line spoken in whichever voice the plugin falls back to'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/voices/{voiceId}/sample':
        get:
            operationId: getVoiceSample
            summary: 'Get voice sample'
            description: 'A short line spoken in one voice, so an operator can hear it before choosing it'
            parameters:
                - name: voiceId
                  in: path
                  required: true
                  schema:
                      type: string
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/voices/preview':
        post:
            operationId: previewSpeech
            summary: 'Preview speech'
            description: "Speaks the caller's words in one voice, so a break can be heard before it is written for air"
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/SpeechPreviewRequest'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
    '/segments/{id}':
        delete:
            operationId: deleteSegment
            summary: 'Delete segment'
            description: 'Removes a recording and the inbox file behind it, so the next scan does not read it back in'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/SegmentList'
                '404':
                    description: 'Not found'
                '409':
                    description: Conflict
    '/segments/{id}/audio':
        get:
            operationId: getSegmentAudio
            summary: 'Get segment audio'
            description: 'The audio of one segment'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/audio/{checksum}/{ext}':
        get:
            operationId: getStoredAudio
            summary: 'Get stored audio'
            description: 'Audio out of the segment store, addressed by content rather than by row'
            parameters:
                - name: checksum
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 64
                      maxLength: 64
                - name: ext
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 8
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/pronunciations':
        get:
            operationId: listPronunciations
            summary: 'List pronunciations'
            description: "The station's lexicon: what it says, what has been proposed to it, and what it has turned down"
            parameters:
                - name: PronunciationQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/PronunciationQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PronunciationList'
        post:
            operationId: createPronunciation
            summary: 'Create pronunciation'
            description: 'Adds one the operator typed. It is said from the next render on'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PronunciationWrite'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PronunciationList'
    '/pronunciations/{id}':
        put:
            operationId: updatePronunciation
            summary: 'Update pronunciation'
            description: "Rewrites one entry's words, whoever proposed it"
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PronunciationWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PronunciationList'
        delete:
            operationId: deletePronunciation
            summary: 'Delete pronunciation'
            description: 'Removes an entry outright. Turning a PROPOSAL down is a state rather than a deletion, because a deleted one comes back on the next pass'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PronunciationList'
    '/pronunciations/{id}/state':
        put:
            operationId: setPronunciationState
            summary: 'Set pronunciation state'
            description: 'Accepts a proposal, turns one down, or takes an entry out of use without losing what it said'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PronunciationStateWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PronunciationList'
    '/pads':
        get:
            operationId: listPads
            summary: 'List pads'
            description: 'Every sound the station holds, board by board'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
        post:
            operationId: uploadPad
            summary: 'Upload pad'
            description: 'Takes a sound in from the browser and puts it on a board. The file lands in the pad library on disk, so it survives a rebuild and an archive carries it'
            requestBody:
                required: true
                content:
                    'multipart/form-data':
                        schema:
                            '$ref': '#/components/schemas/PadUpload'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
                '400':
                    description: 'Bad request'
                '409':
                    description: Conflict
                '413':
                    description: 'Response 413'
                '415':
                    description: 'Response 415'
    '/pads/scan':
        post:
            operationId: scanPads
            summary: 'Scan the pad library'
            description: 'Takes whatever audio is sitting in the pad library directory onto its board. Safe to repeat: a file nobody has touched is seen and left alone'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadScanResult'
    '/pads/fetch':
        post:
            operationId: fetchPad
            summary: 'Fetch pad'
            description: 'Fetches a sound from an address and puts it on a board. The operator names the address, so this is them choosing a file exactly as dropping one in the library is'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PadFetch'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
                '400':
                    description: 'Bad request'
                '409':
                    description: Conflict
                '413':
                    description: 'Response 413'
                '415':
                    description: 'Response 415'
                '502':
                    description: 'Response 502'
    '/pads/{id}':
        delete:
            operationId: deletePad
            summary: 'Delete pad'
            description: 'Removes a sound the console put there, and the file it wrote for it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
                '404':
                    description: 'Not found'
                '409':
                    description: Conflict
    '/pads/{id}/state':
        put:
            operationId: setPadState
            summary: 'Set pad state'
            description: 'Turns a sound down, or puts one back. Answers the whole rack, since one pad changing state is one row moving between two sections of the same page'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PadState'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
    '/pads/{id}/audio':
        get:
            operationId: getPadAudio
            summary: 'Get pad audio'
            description: 'The sound itself, so an operator can hear what they dropped in'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'audio/mpeg':
                            schema:
                                type: string
                                format: binary
                        'audio/wav':
                            schema:
                                type: string
                                format: binary
                        'audio/ogg':
                            schema:
                                type: string
                                format: binary
                        'audio/flac':
                            schema:
                                type: string
                                format: binary
                        'audio/mp4':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                        etag:
                            schema:
                                type: string
                '304':
                    description: 'Response 304'
    '/pads/sets':
        post:
            operationId: createPadSet
            summary: 'Create pad set'
            description: 'Names a new set, or answers the one already under that key'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PadSetWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
    '/pads/sets/{id}':
        put:
            operationId: updatePadSet
            summary: 'Update pad set'
            description: 'Renames a set. The KEY moves with it, so every persona naming the old one stops finding it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PadSetWrite'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
        delete:
            operationId: deletePadSet
            summary: 'Delete pad set'
            description: 'Removes a set and its memberships, and no pads at all'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
    '/pads/sets/{id}/pads':
        put:
            operationId: setPadMembership
            summary: 'Set pad membership'
            description: 'Puts a pad on a set or takes it off. Refused where the set already answers to that name, because a script writes a name'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      format: uuid
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/PadSetMembership'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/PadList'
    '/schedule':
        get:
            operationId: list
            summary: 'List schedule'
            description: "Every slot in this station's schedule, earliest in the day first"
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleSlotList'
        post:
            operationId: create
            summary: 'Create schedule slot'
            description: 'Adds a slot. The station does not change over until its start time comes round'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ScheduleSlot'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleSlotList'
    '/schedule/current':
        get:
            operationId: current
            summary: 'Read current slot'
            description: 'Which slot the clock says should be on, and which one the station is actually airing'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleNow'
    '/schedule/timetable':
        get:
            operationId: timetable
            summary: 'Read timetable'
            description: "The station's day as blocks, contiguous and gapless, for drawing"
            parameters:
                - name: ScheduleTimetableQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/ScheduleTimetableQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleTimetable'
    '/schedule/{id}':
        put:
            operationId: update
            summary: 'Update schedule slot'
            description: 'Rewrites a slot. Takes effect at its next boundary rather than immediately'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/ScheduleSlot'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleSlotList'
        delete:
            operationId: remove
            summary: 'Delete schedule slot'
            description: 'Removes a slot. Whatever is on air stays on until the next slot begins'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/ScheduleSlotList'
    '/settings':
        get:
            operationId: read
            summary: 'Get settings'
            description: 'Every station setting, its descriptor and its current value'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationSettings'
        put:
            operationId: writeSubmitted
            summary: 'Update settings'
            description: 'Applies a submitted settings form and answers with the settings as they now stand'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/StationSettingsInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationSettings'
    '/logs':
        get:
            operationId: listSources
            summary: 'List logs'
            description: 'Every log this install has, present or not, with its size and when it was last written'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/LogSourceList'
    '/logs/{id}':
        get:
            operationId: readLog
            summary: 'Read log'
            description: 'A tail of one log, newest first'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 40
                - name: LogQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/LogQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/LogPage'
                '404':
                    description: 'Not found'
    '/logs/{id}/download':
        get:
            operationId: downloadLog
            summary: 'Download log'
            description: 'The retained log as a plain-text attachment, oldest first, as the file is written'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 40
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'text/plain':
                            schema:
                                type: string
                    headers:
                        Content-Disposition:
                            schema:
                                type: string
                '404':
                    description: 'Not found'
    '/station/attention':
        get:
            operationId: read
            summary: 'Read station attention'
            description: 'Everything wrong or waiting, worst first, each with the console page that can act on it'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationAttention'
    '/station/checkup':
        get:
            operationId: read
            summary: 'Read station checkup'
            description: 'The loops the station runs and how much of the library it has looked at'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StationCheckup'
    '/traces':
        get:
            operationId: readTraces
            summary: 'Read traces'
            description: 'Recent decisions, newest first, folded to one row each'
            parameters:
                - name: TracesQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/TracesQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TracesPage'
    '/traces/{id}':
        get:
            operationId: readTrace
            summary: 'Read trace'
            description: 'One decision: every call it made, and the decisions on either side of it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 200
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TraceDetail'
                '404':
                    description: 'Not found'
    '/storage':
        get:
            operationId: readStorage
            summary: 'Read storage'
            description: 'What is on disk, per store, against what the database says should be'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/StorageReport'
    '/hls/{name}':
        get:
            operationId: getHlsPlaylist
            summary: 'Get HLS playlist'
            description: 'One HLS playlist, and the tick that says somebody is still listening to it'
            parameters:
                - name: name
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 120
            security: []
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/vnd.apple.mpegurl':
                            schema:
                                type: string
                                format: binary
                    headers:
                        cache-control:
                            schema:
                                type: string
                '404':
                    description: 'Not found'
    '/stream/authorization':
        get:
            operationId: readAuthorization
            summary: 'Read fetcher authorization'
            description: 'What the track fetcher holds by way of a Spotify login, and whether an authorization is already waiting to be finished'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/FetcherAuthorization'
        post:
            operationId: startAuthorization
            summary: 'Start fetcher authorization'
            description: "Starts the fetcher's one-time authorization and answers with the URL to open. Starting another replaces whichever was pending"
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/FetcherAuthorizationStart'
    '/stream/authorization/complete':
        post:
            operationId: finishAuthorization
            summary: 'Finish fetcher authorization'
            description: "Finishes an authorization from the address the operator's browser ended up at"
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/FetcherAuthorizationInput'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/FetcherAuthorizationFinished'
    '/topics':
        get:
            operationId: list
            summary: 'List topics'
            description: 'Every subject this station has named, for one sort of break or for all of them'
            parameters:
                - name: TopicQuery
                  in: query
                  required: false
                  schema:
                      '$ref': '#/components/schemas/TopicQuery'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TopicList'
        post:
            operationId: create
            summary: 'Create topic'
            description: 'Names a new subject. Nothing uses it until something points at it'
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/Topic'
            responses:
                '201':
                    description: Created
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TopicList'
    '/topics/kinds':
        get:
            operationId: kinds
            summary: 'List topic kinds'
            description: "Which sorts of break have subjects, and the form each one's settings are edited with"
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TopicKindList'
    '/topics/{id}':
        put:
            operationId: update
            summary: 'Update topic'
            description: 'Rewrites one subject. A break already written keeps the words it was given'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            requestBody:
                required: true
                content:
                    'application/json':
                        schema:
                            '$ref': '#/components/schemas/Topic'
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TopicList'
        delete:
            operationId: remove
            summary: 'Delete topic'
            description: 'Removes a subject, and any band on the format clock that asked for it'
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                      type: string
                      minLength: 1
                      maxLength: 100
            responses:
                '200':
                    description: 'Successful response'
                    content:
                        'application/json':
                            schema:
                                '$ref': '#/components/schemas/TopicList'
components:
    schemas:
        ActivityModule:
            type: string
            enum: [playout, director, render, catalog, plugins]
            description: "Which part of the station an entry came from, and the console's one filter axis"
        ActivitySeverity:
            type: string
            enum: [info, warn, fault]
            description: "How an entry reads, not how bad it is. There is deliberately no `waiting`: a station idling for\nwant of a listener says so in its own words and stays `info`, for the same reason the transport\nreports it as `ready` rather than as a mild fault"
        ActivityEntry:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Unique across the whole feed, and half of the cursor below'
                at:
                    type: string
                    format: date-time
                    description: 'When it happened, as the database recorded it'
                module:
                    '$ref': '#/components/schemas/ActivityModule'
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Dotted and stable: `silence.cause`, `air.on`, `segment.ready`, `track.aired`. What a console draws a line with, never something a decision is made on'
                severity:
                    '$ref': '#/components/schemas/ActivitySeverity'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: 'The sentence a person reads, phrased by whatever produced it'
                data:
                    type: object
                    additionalProperties: {}
                    description: 'The structured half, for a reader that wants to filter or chart rather than read'
                segmentId:
                    type: string
                    maxLength: 100
                    description: 'The segment this is about, for an entry that came from one'
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The catalog track this is about, for an entry that came from one'
            required: [id, at, module, kind, severity, detail]
            description: "One thing that happened, from whichever of the feed's sources holds it"
        ActivityQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 200
                before:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Where the previous page ended. Opaque, and a keyset rather than an offset because rows arrive at the head continuously: an offset would re-show a row on every page as the feed grew under it. Pass back whatever `nextBefore` said and nothing else'
                module:
                    '$ref': '#/components/schemas/ActivityModule'
                minSeverity:
                    '$ref': '#/components/schemas/ActivitySeverity'
                    description: 'The floor, not the exact match: `warn` answers with warnings and faults. Absent is everything'
            description: 'One page of the feed, newest first'
        ActivityPage:
            type: object
            properties:
                entries:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ActivityEntry'
                nextBefore:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The cursor for the page after this one, absent once the feed has been read to its end'
            required: [entries]
        AuthenticationGrantType:
            type: string
            enum:
                - client_credentials
                - password
                - refresh_token
                - link
                - code
                - fido
                - authenticator
                - oidc
            description: 'Denotes the authorization flow to use'
        PasswordlessAuthenticationGrantType:
            type: string
            enum: [link, code, fido, oidc]
            description: 'Denotes the authorization flow to use'
        AuthenticationFactorMethod:
            type: string
            enum: [phone, password, email, authenticator, fido, oidc]
            description: 'The type of the factor'
        AuthenticationFactorKind:
            type: string
            enum: [knowledge, possession, biometric]
            description: 'The kind of the factor'
        OidcProvider:
            type: string
            enum: [google]
            description: 'The OIDC identity provider'
        BaseAuthenticationRequest:
            type: object
            properties:
                grant_type:
                    '$ref': '#/components/schemas/AuthenticationGrantType'
                    description: 'The grant type for the request'
                scope:
                    type: string
                    maxLength: 100
                    description: 'The scope of the request'
                client_id:
                    type: string
                    format: uuid
                    description: "The application's client identifier, if available"
            required: [grant_type]
        ClientCredentialsAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: client_credentials
                          description: 'The grant type for the request'
                      client_id:
                          type: string
                          format: uuid
                          description: 'The client identifier'
                      client_secret:
                          type: string
                          minLength: 8
                          maxLength: 256
                          description: 'The client secret'
                  required: [grant_type, client_id, client_secret]
        PasswordAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: password
                          description: 'The grant type for the request'
                      username:
                          type: string
                          minLength: 3
                          maxLength: 64
                          description: "User's identifier, usually an email address"
                      password:
                          type: string
                          minLength: 8
                          maxLength: 256
                          description: "User's password"
                  required: [grant_type, username, password]
        RefreshTokenAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: refresh_token
                          description: 'The grant type for the request'
                      refresh_token:
                          type: string
                          description: 'The refresh token issued by the authorization server. Optional: browser clients omit it and present the httpOnly refresh cookie instead'
                  required: [grant_type]
        LinkAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: link
                          description: 'The grant type for the request'
                      challenge_id:
                          type: string
                          maxLength: 100
                          description: 'The email challenge id returned by `POST /auth/login/start` — binds the link to the issued challenge so cross-device clicks work'
                      link:
                          type: string
                          description: 'The magic link token'
                  required: [grant_type, challenge_id, link]
        CodeAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: code
                          description: 'The grant type for the request'
                      code:
                          type: string
                          minLength: 6
                          maxLength: 10
                          description: 'The one-time code'
                      code_verifier:
                          type: string
                          minLength: 43
                          maxLength: 128
                          description: 'PKCE verifier — required for a primary code login; absent when mfa_challenge_id is set'
                      mfa_challenge_id:
                          type: string
                          maxLength: 100
                          description: 'When set, completes a pending MFA challenge; replaces code_verifier as proof-of-origin'
                      challenge_id:
                          type: string
                          maxLength: 100
                          description: 'The phone/email challenge id (returned by POST /auth/factors/start for phone-MFA). Required when mfa_challenge_id is set; ignored otherwise (resolved via PKCE)'
                  required: [grant_type, code]
        FidoAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: fido
                          description: 'The grant type for the request'
                      credential:
                          '$ref': '#/components/schemas/PublicKeyCredentialWithAssertion'
                          description: 'A passkey credential object'
                      challenge_id:
                          type: string
                          maxLength: 100
                          description: 'The FIDO assertion challenge id returned by `POST /auth/login/start` (primary) or `POST /auth/factors/start` (MFA second factor). Must be the per-challenge id, not the actor id.'
                      mfa_challenge_id:
                          type: string
                          maxLength: 100
                          description: 'When set, completes a pending MFA challenge instead of issuing a single-factor session'
                  required: [grant_type, credential, challenge_id]
        AuthenticatorAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: authenticator
                          description: 'The grant type for the request'
                      code:
                          type: string
                          minLength: 6
                          maxLength: 10
                          description: 'The TOTP code'
                      mfa_challenge_id:
                          type: string
                          maxLength: 100
                          description: 'The pending MFA challenge — required, TOTP has no other actor binding at initial login'
                      method_id:
                          type: string
                          description: "The id of the enrolled authenticator factor to verify against (must be present in the MFA challenge's eligible list)"
                  required: [grant_type, code, mfa_challenge_id, method_id]
        OidcAuthenticationRequest:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationRequest'
                - type: object
                  properties:
                      grant_type:
                          const: oidc
                          description: 'The grant type for the request'
                      challenge_id:
                          type: string
                          maxLength: 100
                          description: 'The one-time stash id from the OIDC callback redirect (the value after `?token=oidc:` on `/auth/callback`). Single-use — the API consumes it via `OidcFactorService.redeemAuthenticatedExchange`.'
                  required: [grant_type, challenge_id]
        AuthenticationRequest:
            oneOf:
                - '$ref': '#/components/schemas/PasswordAuthenticationRequest'
                - '$ref': '#/components/schemas/ClientCredentialsAuthenticationRequest'
                - '$ref': '#/components/schemas/RefreshTokenAuthenticationRequest'
                - '$ref': '#/components/schemas/LinkAuthenticationRequest'
                - '$ref': '#/components/schemas/CodeAuthenticationRequest'
                - '$ref': '#/components/schemas/FidoAuthenticationRequest'
                - '$ref': '#/components/schemas/AuthenticatorAuthenticationRequest'
                - '$ref': '#/components/schemas/OidcAuthenticationRequest'
            discriminator:
                propertyName: grant_type
                mapping:
                    password: '#/components/schemas/PasswordAuthenticationRequest'
                    client_credentials: '#/components/schemas/ClientCredentialsAuthenticationRequest'
                    refresh_token: '#/components/schemas/RefreshTokenAuthenticationRequest'
                    link: '#/components/schemas/LinkAuthenticationRequest'
                    code: '#/components/schemas/CodeAuthenticationRequest'
                    fido: '#/components/schemas/FidoAuthenticationRequest'
                    authenticator: '#/components/schemas/AuthenticatorAuthenticationRequest'
                    oidc: '#/components/schemas/OidcAuthenticationRequest'
        AuthenticationToken:
            type: object
            properties:
                accessToken:
                    type: string
                    description: 'The access token string as issued by the authorization server'
                refreshToken:
                    type: string
                    description: 'A refresh token which applications can use to obtain another access token'
                expiresIn:
                    type: integer
                    description: 'Unix timestamp (seconds) when the access token expires'
                tokenType:
                    type: string
                    description: 'The type of token this is, typically just the string *Bearer*'
                scope:
                    type: string
                    description: 'Space-separated list of scopes granted to this token'
            required: [accessToken, expiresIn, tokenType, scope]
            description: 'Represents an authentication token'
        AuthenticationTokenIssued:
            type: object
            properties:
                result:
                    const: token
                    description: Discriminator
                accessToken:
                    type: string
                    description: 'The access token string as issued by the authorization server'
                refreshToken:
                    type: string
                    description: 'A refresh token which applications can use to obtain another access token'
                expiresIn:
                    type: integer
                    description: 'Unix timestamp (seconds) when the access token expires'
                tokenType:
                    type: string
                    description: 'The type of token this is, typically just the string *Bearer*'
                scope:
                    type: string
                    description: 'Space-separated list of scopes granted to this token'
            required: [result, accessToken, expiresIn, tokenType, scope]
            description: 'Issued-token arm of /auth/token response'
        MfaChallengeFactor:
            type: object
            properties:
                method:
                    '$ref': '#/components/schemas/AuthenticationFactorMethod'
                    description: 'The factor method'
                methodId:
                    type: string
                    description: "The id of the enrolled factor (opaque to the SPA, must be echoed back in the proof for methods that don't bind another way)"
                kind:
                    '$ref': '#/components/schemas/AuthenticationFactorKind'
                    description: 'The factor kind (knowledge, possession, biometric) — the SPA filters against step-up `acceptableKinds`/`excludeKinds` hints'
                label:
                    type: string
                    description: 'Optional human-readable label (e.g. provider name for OIDC, friendly name for FIDO)'
            required: [method, methodId, kind]
            description: 'A factor the SPA may use to satisfy the MFA challenge'
        MfaRequiredResponse:
            type: object
            properties:
                result:
                    const: mfa_required
                    description: Discriminator
                challengeId:
                    type: string
                    maxLength: 100
                    description: 'The MFA challenge identifier — pass back as `mfa_challenge_id` on the proof grant'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'When the MFA challenge expires'
                factors:
                    type: array
                    items:
                        '$ref': '#/components/schemas/MfaChallengeFactor'
                    description: 'Eligible factors the SPA may use to complete the challenge'
            required: [result, challengeId, expiresAt, factors]
            description: 'MFA-required arm of /auth/token response'
        EnrollmentRequiredResponse:
            type: object
            properties:
                result:
                    const: enrollment_required
                    description: Discriminator
            required: [result]
            description: 'Returned by /auth/step-up/start when no enrolled factor satisfies the requirement. The SPA should drive the user through enrollment and retry the gated action afterwards.'
        StepUpStartResponse:
            oneOf:
                - '$ref': '#/components/schemas/MfaRequiredResponse'
                - '$ref': '#/components/schemas/EnrollmentRequiredResponse'
            discriminator:
                propertyName: result
                mapping:
                    mfa_required: '#/components/schemas/MfaRequiredResponse'
                    enrollment_required: '#/components/schemas/EnrollmentRequiredResponse'
        AuthenticationTokenResponse:
            oneOf:
                - '$ref': '#/components/schemas/AuthenticationTokenIssued'
                - '$ref': '#/components/schemas/MfaRequiredResponse'
            discriminator:
                propertyName: result
                mapping:
                    token: '#/components/schemas/AuthenticationTokenIssued'
                    mfa_required: '#/components/schemas/MfaRequiredResponse'
        PublicKeyCredential:
            type: object
            properties:
                id:
                    type: string
                    description: 'The base64url encoding of `rawId`'
                type:
                    const: public-key
                    description: 'This enumeration defines the valid credential types. It is an extension point; values can be added to it in the future, as more credential types are defined. The values of this enumeration are used for versioning the Authentication Assertion and attestation structures according to the type of the authenticator. Currently one credential type is defined, namely `public-key`.'
                rawId:
                    type: string
                    description: 'The credential identifier'
                authenticatorAttachment:
                    type: string
                    enum: [cross-platform, platform]
                    description: 'The authenticator attachment'
            required: [id, type, rawId]
            description: 'Represents a common shape of a `PublicKeyCredential` after the client serializes the `id` and `rawId` fields to base64 strings for transport'
        SimpleClientExtensionResults:
            type: object
            properties:
                appid:
                    type: boolean
                    description: 'Whether the client is an application'
                appidExclude:
                    type: boolean
                    description: 'Whether the client is excluded from appid verification'
                credProps:
                    type: object
                    properties:
                        rk:
                            type: boolean
                            description: 'Whether the client supports the rk extension'
                    required: [rk]
            description: 'Subset of the WebAuthn client extension results the service round-trips'
        FidoAuthenticatorAssertionResponse:
            type: object
            properties:
                clientDataJSON:
                    type: string
                    description: 'The client data JSON'
                authenticatorData:
                    type: string
                    description: 'The authenticator data'
                signature:
                    type: string
                    description: 'The signature'
                userHandle:
                    type: string
                    description: 'The user handle'
            required: [clientDataJSON, authenticatorData, signature]
        PublicKeyCredentialWithAssertion:
            allOf:
                - '$ref': '#/components/schemas/PublicKeyCredential'
                - type: object
                  properties:
                      clientExtensionResults:
                          '$ref': '#/components/schemas/SimpleClientExtensionResults'
                          description: 'The client extension results'
                      response:
                          '$ref': '#/components/schemas/FidoAuthenticatorAssertionResponse'
                          description: 'The authenticator assertion response'
                  required: [clientExtensionResults, response]
        AuthenticationRegistration:
            type: object
            properties:
                registrationId:
                    type: string
                    readOnly: true
                    description: 'The registration identifier'
                expiresAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'The registration expiration timestamp'
                email:
                    type: string
                    format: email
                    writeOnly: true
                    description: "User's email address"
                password:
                    type: string
                    minLength: 8
                    maxLength: 256
                    writeOnly: true
                    description: 'optionally set a password for the user'
            required: [registrationId, expiresAt, email]
        AuthenticationRegistrationVerification:
            type: object
            properties:
                registrationId:
                    type: string
                    description: 'The registration identifier'
                code:
                    type: string
                    minLength: 6
                    maxLength: 10
                    description: 'The verification code'
            required: [registrationId, code]
        BaseAuthenticationLoginStart:
            type: object
            properties:
                grant_type:
                    '$ref': '#/components/schemas/PasswordlessAuthenticationGrantType'
                    description: 'The grant type for the request'
                client_id:
                    type: string
                    format: uuid
                    description: "The application's client identifier, if available"
            required: [grant_type]
        BaseAuthenticationLoginStartWithEmail:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStart'
                - type: object
                  properties:
                      email:
                          type: string
                          format: email
                          description: "User's email address"
                  required: [email]
        LinkAuthenticationLoginStart:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartWithEmail'
                - type: object
                  properties:
                      grant_type:
                          const: link
                          description: 'The grant type for the request'
                  required: [grant_type]
        CodeAuthenticationLoginStart:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartWithEmail'
                - type: object
                  properties:
                      grant_type:
                          const: code
                          description: 'The grant type for the request'
                      code_challenge:
                          type: string
                          maxLength: 100
                          description: 'A base64url encoded SHA256 hash of a one time secret used to validate that the request starts and ends on the same device'
                  required: [grant_type, code_challenge]
        FidoAuthenticationLoginStart:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartWithEmail'
                - type: object
                  properties:
                      grant_type:
                          const: fido
                          description: 'The grant type for the request'
                  required: [grant_type]
        OidcAuthenticationLoginStart:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStart'
                - type: object
                  properties:
                      grant_type:
                          const: oidc
                          description: 'The grant type for the request'
                      provider:
                          '$ref': '#/components/schemas/OidcProvider'
                          description: 'The IdP to authorize against'
                  required: [grant_type, provider]
        AuthenticationLoginStart:
            oneOf:
                - '$ref': '#/components/schemas/LinkAuthenticationLoginStart'
                - '$ref': '#/components/schemas/CodeAuthenticationLoginStart'
                - '$ref': '#/components/schemas/FidoAuthenticationLoginStart'
                - '$ref': '#/components/schemas/OidcAuthenticationLoginStart'
            discriminator:
                propertyName: grant_type
                mapping:
                    link: '#/components/schemas/LinkAuthenticationLoginStart'
                    code: '#/components/schemas/CodeAuthenticationLoginStart'
                    fido: '#/components/schemas/FidoAuthenticationLoginStart'
                    oidc: '#/components/schemas/OidcAuthenticationLoginStart'
        PublicKeyCredentialDescriptor:
            type: object
            properties:
                type:
                    const: public-key
                    description: 'The credential type — currently always `public-key`'
                id:
                    type: string
                    description: 'The base64url-encoded credential identifier'
                transports:
                    type: array
                    items:
                        type: string
                        enum: [usb, nfc, ble, internal, hybrid]
                    description: 'Transports the authenticator advertises'
            required: [type, id]
            description: 'A credential the relying party expects the user to be able to present'
        BaseAuthenticationLoginStartResponse:
            type: object
            properties:
                grant_type:
                    '$ref': '#/components/schemas/PasswordlessAuthenticationGrantType'
                    description: 'The grant type for the response'
                challengeId:
                    type: string
                    maxLength: 100
                    description: 'The challenge identifier'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'The challenge expiration timestamp'
            required: [grant_type, challengeId, expiresAt]
        CodeAuthenticationLoginStartResponse:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartResponse'
                - type: object
                  properties:
                      grant_type:
                          const: code
                          description: 'The grant type for the response'
                  required: [grant_type]
        LinkAuthenticationLoginStartResponse:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartResponse'
                - type: object
                  properties:
                      grant_type:
                          const: link
                          description: 'The grant type for the response'
                  required: [grant_type]
        FidoPublicKeyCredentialRequestOptions:
            type: object
            properties:
                challenge:
                    type: string
                timeout:
                    type: integer
                    description: 'WebAuthn timeout hint in milliseconds'
                rpId:
                    type: string
                attestation:
                    type: string
                    enum: [direct, indirect, none]
                    description: 'The attestation'
                userVerification:
                    type: string
                    enum: [required, preferred, discouraged]
                    description: 'Whether the authenticator must verify the user'
                rawChallenge:
                    type: string
                    format: binary
                    description: 'The raw challenge'
                extensions:
                    type: object
                allowCredentials:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PublicKeyCredentialDescriptor'
            required: [challenge]
        FidoAuthenticationLoginStartResponse:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartResponse'
                - type: object
                  properties:
                      grant_type:
                          const: fido
                          description: 'The grant type for the response'
                      assertion:
                          '$ref': '#/components/schemas/FidoPublicKeyCredentialRequestOptions'
                          description: 'The WebAuthn assertion options'
                  required: [grant_type, assertion]
        OidcAuthenticationLoginStartResponse:
            allOf:
                - '$ref': '#/components/schemas/BaseAuthenticationLoginStartResponse'
                - type: object
                  properties:
                      grant_type:
                          const: oidc
                          description: 'The grant type for the response'
                      authorize_url:
                          type: string
                          description: 'Fully-formed authorize URL the user-agent should be redirected to'
                      state:
                          type: string
                          maxLength: 200
                          description: 'Opaque state token bound to this authorization round-trip'
                  required: [grant_type, authorize_url, state]
        AuthenticationLoginStartResponse:
            oneOf:
                - '$ref': '#/components/schemas/CodeAuthenticationLoginStartResponse'
                - '$ref': '#/components/schemas/LinkAuthenticationLoginStartResponse'
                - '$ref': '#/components/schemas/FidoAuthenticationLoginStartResponse'
                - '$ref': '#/components/schemas/OidcAuthenticationLoginStartResponse'
            discriminator:
                propertyName: grant_type
                mapping:
                    code: '#/components/schemas/CodeAuthenticationLoginStartResponse'
                    link: '#/components/schemas/LinkAuthenticationLoginStartResponse'
                    fido: '#/components/schemas/FidoAuthenticationLoginStartResponse'
                    oidc: '#/components/schemas/OidcAuthenticationLoginStartResponse'
        FidoAuthenticatorTransport:
            type: string
            enum: [hybrid, ble, internal, nfc, usb]
            description: 'The transport used by the authenticator'
        FidoAuthenticatorAttestationResponse:
            type: object
            properties:
                clientDataJSON:
                    type: string
                    description: 'The client data JSON'
                attestationObject:
                    type: string
                    description: 'The attestation object'
                transports:
                    type: array
                    items:
                        '$ref': '#/components/schemas/FidoAuthenticatorTransport'
                    description: 'The transports used by the authenticator'
            required: [clientDataJSON, attestationObject]
            description: 'Serialized form of `AuthenticatorAttestationResponse` — produced by the browser at registration; all binary fields are base64-encoded for transport'
        PublicKeyCredentialWithAttestation:
            allOf:
                - '$ref': '#/components/schemas/PublicKeyCredential'
                - type: object
                  properties:
                      clientExtensionResults:
                          '$ref': '#/components/schemas/SimpleClientExtensionResults'
                          description: 'The client extension results'
                      response:
                          '$ref': '#/components/schemas/FidoAuthenticatorAttestationResponse'
                          description: 'The authenticator attestation response'
                  required: [clientExtensionResults, response]
        AuthenticationFactor:
            type: object
            properties:
                method:
                    '$ref': '#/components/schemas/AuthenticationFactorMethod'
                    description: 'The method of the factor'
                kind:
                    '$ref': '#/components/schemas/AuthenticationFactorKind'
                    description: 'The kind of the factor'
                methodId:
                    type: string
                    description: 'The method identifier'
                label:
                    type: string
                    description: 'The label for the factor'
            required: [method, kind, methodId]
        FactorChallengePhoneStart:
            type: object
            properties:
                method:
                    const: phone
                    description: Discriminator
                transport:
                    const: sms
                    description: 'Delivery channel — only `sms` is supported in this phase'
                mfa_challenge_id:
                    type: string
                    maxLength: 100
                    description: 'The MFA challenge to which this factor challenge is bound'
            required: [method, transport, mfa_challenge_id]
            description: 'Issue a phone SMS challenge during a pending MFA round'
        FactorChallengeFidoStart:
            type: object
            properties:
                method:
                    const: fido
                    description: Discriminator
                mfa_challenge_id:
                    type: string
                    maxLength: 100
                    description: 'The MFA challenge to which this factor challenge is bound'
            required: [method, mfa_challenge_id]
            description: 'Issue a WebAuthn assertion challenge during a pending MFA round'
        FactorChallengeEmailStart:
            type: object
            properties:
                method:
                    const: email
                    description: Discriminator
                mfa_challenge_id:
                    type: string
                    maxLength: 100
                    description: 'The MFA challenge to which this factor challenge is bound'
            required: [method, mfa_challenge_id]
            description: 'Issue an email one-time-code challenge during a pending MFA round. Always a code: a magic link cannot complete an MFA round, since the `code` grant that redeems one takes `code(min=6, max=10)` and a link token is 43 characters'
        FactorChallengeStartRequest:
            oneOf:
                - '$ref': '#/components/schemas/FactorChallengePhoneStart'
                - '$ref': '#/components/schemas/FactorChallengeFidoStart'
                - '$ref': '#/components/schemas/FactorChallengeEmailStart'
            discriminator:
                propertyName: method
                mapping:
                    phone: '#/components/schemas/FactorChallengePhoneStart'
                    fido: '#/components/schemas/FactorChallengeFidoStart'
                    email: '#/components/schemas/FactorChallengeEmailStart'
        FactorChallengePhoneStartResponse:
            type: object
            properties:
                method:
                    const: phone
                    description: Discriminator
                transport:
                    const: sms
                    description: 'Echo of the chosen delivery channel'
                phoneChallengeId:
                    type: string
                    maxLength: 100
                    description: 'The phone-factor challenge id — echo back on the `code` grant as `challenge_id`'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'When the phone challenge expires'
            required: [method, transport, phoneChallengeId, expiresAt]
            description: 'Response for a phone SMS challenge'
        FactorChallengeFidoStartResponse:
            type: object
            properties:
                method:
                    const: fido
                    description: Discriminator
                fidoChallengeId:
                    type: string
                    maxLength: 100
                    description: 'The FIDO-factor challenge id — echo back on the `fido` grant as `challenge_id`'
                assertion:
                    '$ref': '#/components/schemas/FidoPublicKeyCredentialRequestOptions'
                    description: 'WebAuthn assertion options for navigator.credentials.get'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'When the FIDO challenge expires'
            required: [method, fidoChallengeId, assertion, expiresAt]
            description: 'Response for a FIDO assertion challenge'
        FactorChallengeEmailStartResponse:
            type: object
            properties:
                method:
                    const: email
                    description: Discriminator
                emailChallengeId:
                    type: string
                    maxLength: 100
                    description: 'The email-factor challenge id — echo back on the `code` grant as `challenge_id`'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'When the email challenge expires'
            required: [method, emailChallengeId, expiresAt]
            description: 'Response for an email one-time-code challenge'
        FactorChallengeStartResponse:
            oneOf:
                - '$ref': '#/components/schemas/FactorChallengePhoneStartResponse'
                - '$ref': '#/components/schemas/FactorChallengeFidoStartResponse'
                - '$ref': '#/components/schemas/FactorChallengeEmailStartResponse'
            discriminator:
                propertyName: method
                mapping:
                    phone: '#/components/schemas/FactorChallengePhoneStartResponse'
                    fido: '#/components/schemas/FactorChallengeFidoStartResponse'
                    email: '#/components/schemas/FactorChallengeEmailStartResponse'
        StepUpStartRequest:
            type: object
            properties:
                acceptableMethods:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AuthenticationFactorMethod'
                    description: 'If set, only these factor methods are listed as eligible'
                acceptableKinds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AuthenticationFactorKind'
                    description: 'If set, only these factor kinds are listed as eligible'
                excludeMethods:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AuthenticationFactorMethod'
                    description: 'If set, factors with these methods are never listed'
            description: 'Mint a fresh MFA challenge for the current session so the SPA can satisfy a `step_up_required` denial. Filters mirror `StepUpRequirement` from `@maroonedsoftware/policies`.'
        PlatformRole:
            type: string
            enum: [admin, listener]
            description: 'A platform-wide role held on `platform:main`. `admin` grants every operation; `listener` grants the reads'
        AuthSession:
            type: object
            properties:
                actorId:
                    type: string
                    maxLength: 100
                    description: 'The actor the session belongs to'
                roles:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PlatformRole'
                    description: 'Every platform role the caller holds, sorted. Empty for an account nobody has granted one, which today is any account that did not come in through onboarding'
            required: [actorId, roles]
            description: 'Who the caller is, as the station sees them'
        PhoneFactorRegistration:
            type: object
            properties:
                method:
                    const: phone
                    description: 'The method of the factor'
                value:
                    type: string
                    pattern: '^\+[1-9]\d{1,14}$'
                    description: 'The phone number in E.164 format (e.g. `+12025550123`)'
                codeChallenge:
                    type: string
                    maxLength: 100
                    description: 'A base64url encoded SHA256 hash of a one time secret used to validate that the request starts and ends on the same device'
            required: [method, value, codeChallenge]
        PasswordFactorRegistration:
            type: object
            properties:
                method:
                    const: password
                    description: 'The method of the factor'
                value:
                    type: string
                    minLength: 8
                    maxLength: 256
                    description: 'The password'
            required: [method, value]
        EmailFactorRegistration:
            type: object
            properties:
                method:
                    const: email
                    description: 'The method of the factor'
                value:
                    type: string
                    format: email
                    description: 'The email address'
                codeChallenge:
                    type: string
                    maxLength: 100
                    description: 'A base64url encoded SHA256 hash of a one time secret used to validate that the request starts and ends on the same device'
            required: [method, value, codeChallenge]
        AuthenticatorFactorRegistration:
            type: object
            properties:
                method:
                    const: authenticator
                    description: 'The method of the factor'
                codeChallenge:
                    type: string
                    maxLength: 100
                    description: 'A base64url encoded SHA256 hash of a one time secret used to validate that the request starts and ends on the same device'
                label:
                    type: string
                    description: 'The label for the authenticator factor'
            required: [method, codeChallenge]
        FidoFactorRegistration:
            type: object
            properties:
                method:
                    const: fido
                    description: 'The method of the factor'
                label:
                    type: string
                    description: 'The label for the FIDO factor'
            required: [method]
        AuthenticationFactorRegistration:
            oneOf:
                - '$ref': '#/components/schemas/PhoneFactorRegistration'
                - '$ref': '#/components/schemas/PasswordFactorRegistration'
                - '$ref': '#/components/schemas/EmailFactorRegistration'
                - '$ref': '#/components/schemas/AuthenticatorFactorRegistration'
                - '$ref': '#/components/schemas/FidoFactorRegistration'
            discriminator:
                propertyName: method
                mapping:
                    phone: '#/components/schemas/PhoneFactorRegistration'
                    password: '#/components/schemas/PasswordFactorRegistration'
                    email: '#/components/schemas/EmailFactorRegistration'
                    authenticator: '#/components/schemas/AuthenticatorFactorRegistration'
                    fido: '#/components/schemas/FidoFactorRegistration'
        PhoneFactorRegistrationResponse:
            type: object
            properties:
                method:
                    const: phone
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'The expiration timestamp'
                issuedAt:
                    type: string
                    format: date-time
                    description: 'The issuance timestamp'
            required: [method, registrationId, expiresAt, issuedAt]
        PasswordFactorRegistrationResponse:
            type: object
            properties:
                method:
                    const: password
                    description: 'The method of the factor'
                needsReset:
                    type: boolean
                    description: 'Whether the password needs to be reset'
            required: [method, needsReset]
        EmailFactorRegistrationResponse:
            type: object
            properties:
                method:
                    const: email
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'The expiration timestamp'
                issuedAt:
                    type: string
                    format: date-time
                    description: 'The issuance timestamp'
            required: [method, registrationId, expiresAt, issuedAt]
        AuthenticatorFactorRegistrationResponse:
            type: object
            properties:
                method:
                    const: authenticator
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                secret:
                    type: string
                    description: 'The secret for the authenticator'
                uri:
                    type: string
                    description: 'The URI for the authenticator'
                qrCode:
                    type: string
                    description: 'The QR code for the authenticator'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'The expiration timestamp'
                issuedAt:
                    type: string
                    format: date-time
                    description: 'The issuance timestamp'
            required: [method, registrationId, secret, uri, qrCode, expiresAt, issuedAt]
        FidoFactorAttestation:
            type: object
            properties:
                rp:
                    type: object
                    properties:
                        name:
                            type: string
                            description: 'The relying party name'
                        id:
                            type: string
                            description: 'The relying party identifier'
                        icon:
                            type: string
                            description: 'The relying party icon'
                    required: [name, id]
                    description: 'The relying party'
                user:
                    type: object
                    properties:
                        id:
                            type: string
                            description: 'The user identifier'
                        name:
                            type: string
                            description: 'The user name'
                        displayName:
                            type: string
                            description: 'The user display name'
                    required: [id, name, displayName]
                challenge:
                    type: string
                    description: 'The challenge'
                pubKeyCredParams:
                    type: array
                    items:
                        type: object
                        properties:
                            type:
                                const: public-key
                            alg:
                                type: integer
                        required: [type, alg]
                    description: 'The public key credential parameters'
                timeout:
                    type: integer
                    description: 'The timeout'
                attestation:
                    type: string
                    enum: [direct, indirect, none]
                    description: 'The attestation'
            required: [rp, user, challenge, pubKeyCredParams, attestation]
            description: 'The FIDO factor attestation information'
        FidoFactorRegistrationResponse:
            type: object
            properties:
                method:
                    const: fido
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                expiresAt:
                    type: string
                    format: date-time
                    description: 'The expiration timestamp'
                issuedAt:
                    type: string
                    format: date-time
                    description: 'The issuance timestamp'
                attestation:
                    '$ref': '#/components/schemas/FidoFactorAttestation'
                    description: 'The FIDO factor attestation information'
            required: [method, registrationId, expiresAt, issuedAt, attestation]
        AuthenticationFactorRegistrationResponse:
            oneOf:
                - '$ref': '#/components/schemas/PhoneFactorRegistrationResponse'
                - '$ref': '#/components/schemas/PasswordFactorRegistrationResponse'
                - '$ref': '#/components/schemas/EmailFactorRegistrationResponse'
                - '$ref': '#/components/schemas/AuthenticatorFactorRegistrationResponse'
                - '$ref': '#/components/schemas/FidoFactorRegistrationResponse'
            discriminator:
                propertyName: method
                mapping:
                    phone: '#/components/schemas/PhoneFactorRegistrationResponse'
                    password: '#/components/schemas/PasswordFactorRegistrationResponse'
                    email: '#/components/schemas/EmailFactorRegistrationResponse'
                    authenticator: '#/components/schemas/AuthenticatorFactorRegistrationResponse'
                    fido: '#/components/schemas/FidoFactorRegistrationResponse'
        PhoneFactorRegistrationVerification:
            type: object
            properties:
                method:
                    const: phone
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                code:
                    type: string
                    minLength: 6
                    maxLength: 10
                    description: 'The verification code'
                codeVerifier:
                    type: string
                    minLength: 43
                    maxLength: 128
                    description: 'A base64url encoded one time secret used to validate that the request starts and ends on the same device'
            required: [method, registrationId, code, codeVerifier]
        EmailFactorRegistrationVerification:
            type: object
            properties:
                method:
                    const: email
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                code:
                    type: string
                    minLength: 6
                    maxLength: 10
                    description: 'The verification code'
                codeVerifier:
                    type: string
                    minLength: 43
                    maxLength: 128
                    description: 'A base64url encoded one time secret used to validate that the request starts and ends on the same device'
            required: [method, registrationId, code, codeVerifier]
        AuthenticatorFactorRegistrationVerification:
            type: object
            properties:
                method:
                    const: authenticator
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                code:
                    type: string
                    minLength: 6
                    maxLength: 10
                    description: 'The verification code'
                codeVerifier:
                    type: string
                    minLength: 43
                    maxLength: 128
                    description: 'A base64url encoded one time secret used to validate that the request starts and ends on the same device'
            required: [method, registrationId, code, codeVerifier]
        FidoFactorRegistrationVerification:
            type: object
            properties:
                method:
                    const: fido
                    description: 'The method of the factor'
                registrationId:
                    type: string
                    description: 'The registration identifier'
                credential:
                    '$ref': '#/components/schemas/PublicKeyCredentialWithAttestation'
                    description: 'The credential the client posts back to complete registration'
            required: [method, registrationId, credential]
        AuthenticationFactorRegistrationVerification:
            oneOf:
                - '$ref': '#/components/schemas/PhoneFactorRegistrationVerification'
                - '$ref': '#/components/schemas/EmailFactorRegistrationVerification'
                - '$ref': '#/components/schemas/AuthenticatorFactorRegistrationVerification'
                - '$ref': '#/components/schemas/FidoFactorRegistrationVerification'
            discriminator:
                propertyName: method
                mapping:
                    phone: '#/components/schemas/PhoneFactorRegistrationVerification'
                    email: '#/components/schemas/EmailFactorRegistrationVerification'
                    authenticator: '#/components/schemas/AuthenticatorFactorRegistrationVerification'
                    fido: '#/components/schemas/FidoFactorRegistrationVerification'
        Rating:
            type: string
            enum: [liked, neutral, disliked]
            description: "What the station has been told about a record. `neutral` is the absence of an opinion rather than\na middling one, and it is what rating something back to nothing means."
        RateInput:
            type: object
            properties:
                rating:
                    '$ref': '#/components/schemas/Rating'
            required: [rating]
            description: "Rate an artist, a record or a song. Ratings are absolute: a dislike anywhere above a track\nexcludes it, and nothing the station programmes may turn that off."
        Artist:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                name:
                    type: string
                mbid:
                    type: string
                    format: uuid
                    description: 'MusicBrainz artist id, absent until enrichment resolves one'
                imageUrl:
                    type: string
                    description: 'Absolute upstream URL, or an API-relative path to the local copy'
                rating:
                    '$ref': '#/components/schemas/Rating'
                    default: neutral
                albumCount:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Unmerged albums credited to this artist'
                trackCount:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Unmerged tracks credited to this artist'
            required: [id, name, rating, albumCount, trackCount]
            description: "The canonical work, not a binding to a provider. `deadair.artists` minus the columns that\nonly ingest cares about: `artist_key` is a match key, and a row with `merged_into_id` set is\nnever read out at all.\n\n`imageUrl` on both contracts below is one field with two spellings. An absolute URL is the\nprovider's own, still hotlinked because nothing has cached it yet; a relative `art/<uuid>` is\nthe station's copy, to be resolved against the API base the client already configures (the API\nmounts at the root and does not know the `/api` prefix the edge adds). Prefer the local one by\ndoing nothing: the switch happens server-side as soon as the art cache pass has the bytes."
        Album:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                name:
                    type: string
                artistId:
                    type: string
                    format: uuid
                    readOnly: true
                artistName:
                    type: string
                    readOnly: true
                    description: 'Joined, so a list renders without a second request per row'
                mbid:
                    type: string
                    format: uuid
                    description: 'MusicBrainz release-group id, absent until enrichment resolves one'
                year:
                    type: integer
                imageUrl:
                    type: string
                    description: 'Absolute upstream URL, or an API-relative path to the local copy'
                rating:
                    '$ref': '#/components/schemas/Rating'
                    default: neutral
                trackCount:
                    type: integer
                    minimum: 0
                    readOnly: true
            required: [id, name, artistId, artistName, rating, trackCount]
        Track:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                title:
                    type: string
                artistId:
                    type: string
                    format: uuid
                    readOnly: true
                artistName:
                    type: string
                    readOnly: true
                albumId:
                    type: string
                    format: uuid
                    readOnly: true
                    description: 'Absent on a single ingested outside any release: `tracks.album_id` is nullable'
                albumName:
                    type: string
                    readOnly: true
                albumImageUrl:
                    type: string
                    readOnly: true
                    description: "The record's cover, in the two spellings `Album.imageUrl` has. Nothing hangs art off a recording"
                artists:
                    type: string
                    description: 'Display credit as written on the release ("X feat. Y"), not a join key'
                genre:
                    type: string
                year:
                    type: integer
                durationMs:
                    type: integer
                    minimum: 0
                rating:
                    '$ref': '#/components/schemas/Rating'
                    default: neutral
            required: [id, title, artistId, artistName, artists, rating]
        TrackBinding:
            type: object
            properties:
                sourceId:
                    type: string
                    format: uuid
                    readOnly: true
                    description: '`track_sources.id`, which is also what the audio URL carries'
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                externalId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    readOnly: true
                playable:
                    type: boolean
                    readOnly: true
                    description: 'False when the provider still knows the record but will not serve it here'
                missingAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'When the station gave up on this copy. Cleared by the next sync that sees it again'
                origin:
                    type: string
                    minLength: 1
                    maxLength: 40
                    readOnly: true
                    description: '`sync` if a playlist walk saw it, `discovered` if something looked it up'
                bitrate:
                    type: integer
                    minimum: 0
                    readOnly: true
                format:
                    type: string
                    maxLength: 100
                    readOnly: true
                lastSeenAt:
                    type: string
                    format: date-time
                    readOnly: true
                byteSize:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'What the station holds of this copy, absent when nothing has ever fetched it.'
                fetchedAt:
                    type: string
                    format: date-time
                    readOnly: true
                lastServedAt:
                    type: string
                    format: date-time
                    readOnly: true
                attempts:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'CONSECUTIVE failures. Reset by a fetch that works'
                lastError:
                    type: string
                    maxLength: 2000
                    readOnly: true
                nextAttemptAt:
                    type: string
                    format: date-time
                    readOnly: true
            required: [sourceId, pluginId, externalId, playable, origin, attempts]
            description: "One provider's copy of a record, with whatever the station holds of it.\n\nPER BINDING and never per track, which is the rule the whole page is built on: one canonical\nrecord may bind to several copies inside one provider, those copies are different files with\ndifferent loudness and different cue points, and the one that airs is the one that was resolved.\nCollapsing them would make \"clear the audio\" ambiguous about which file it took.\n\nThe failure columns are here rather than hidden because that is the question this page exists to\nanswer. A row with `attempts` and no `fetchedAt` is a remembered failure, and `lastError` with\n`nextAttemptAt` is the whole of why a perfectly good-looking record will not play."
        TrackAnalysis:
            type: object
            properties:
                schemaVersion:
                    type: integer
                    minimum: 0
                    readOnly: true
                complete:
                    type: boolean
                    readOnly: true
                analyzer:
                    type: string
                    maxLength: 200
                    readOnly: true
                    description: 'The measuring thing itself, which is not the plugin adapting it'
                analyzerPluginId:
                    type: string
                    maxLength: 200
                    readOnly: true
                analyzedAt:
                    type: string
                    format: date-time
                    readOnly: true
                failedAt:
                    type: string
                    format: date-time
                    readOnly: true
                failureReason:
                    type: string
                    maxLength: 2000
                    readOnly: true
            required: [schemaVersion, complete]
            description: "What the measurement sidecar made of a record.\n\n`complete` is NOT `analyzedAt`, and the two are separate fields for a reason `0005_music.sql`\nargues at length: a measurement of a truncated download is confident and wrong, so every reader in\nthe app filters on `complete` and a page that showed only a date would be reporting a record as\nmeasured that nothing will use the measurement of."
        TrackPlay:
            type: object
            properties:
                airedAt:
                    type: string
                    format: date-time
                    readOnly: true
                broadcastId:
                    type: string
                    format: uuid
                    readOnly: true
                source:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                    description: 'What put it in the running order'
            required: [airedAt, source]
            description: 'One airing of a record, as this page needs it: when, and under which broadcast.'
        TrackDetail:
            allOf:
                - '$ref': '#/components/schemas/Track'
                - type: object
                  properties:
                      bindings:
                          type: array
                          items:
                              '$ref': '#/components/schemas/TrackBinding'
                      analysis:
                          '$ref': '#/components/schemas/TrackAnalysis'
                          description: 'Absent for a record the walk has not reached'
                      plays:
                          type: array
                          items:
                              '$ref': '#/components/schemas/TrackPlay'
                          description: 'The most recent airings, newest first'
                      playCount:
                          type: integer
                          minimum: 0
                          readOnly: true
                          description: 'How many times in all, which the list above is only the head of'
                  required: [bindings, plays, playCount]
        TrackClearResult:
            type: object
            properties:
                trackId:
                    type: string
                    format: uuid
                    readOnly: true
                cleared:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Rows this affected. Zero is an ordinary answer, not a failure'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 400
                    readOnly: true
                    description: 'What happened, in the words the console shows'
            required: [trackId, cleared, detail]
            description: "What a clear actually did.\n\nA count rather than a bare 204, because the interesting answers are the small ones: clearing the\naudio of a record with three copies and being told `1` is the station saying two of them were\nnever here — which is a fact about the record and not about the button."
        ClearEnrichmentQuery:
            type: object
            properties:
                provider:
                    type: string
                    minLength: 1
                    maxLength: 200
            description: "Narrow a clear to one provider's answer, for the case where one source is wrong and the rest are\nnot. Absent clears every provider's."
        CatalogSort:
            type: string
            enum: [name, albums, tracks, year, rating]
            description: "What an artist or album list is ordered BY, where `Pagination.sort` says only which direction.\n\n  name    the default, and the only key every row here has\n  albums  how many records the station holds of them. Artists only\n  tracks  how many songs. Artists only\n  year    when the record came out. Albums only\n  rating  the operator's own opinion\n\nOne enum for both lists rather than two, because the alternative is a second near-identical\ncontract whose only content is which two keys it drops. A key the row cannot answer falls back to\nname order rather than failing: an ordering nobody can serve is a page an operator cannot open."
        CatalogQuery:
            allOf:
                - '$ref': '#/components/schemas/Pagination'
                - type: object
                  properties:
                      search:
                          type: string
                          minLength: 1
                          maxLength: 200
                      sortBy:
                          '$ref': '#/components/schemas/CatalogSort'
        TrackState:
            type: string
            enum: [cached, uncached, unmeasured, benched, failing]
            description: "Which records to show, by what the station has of them rather than by what they are.\n\n  cached      the audio is on this machine, so it can be committed to the running order now\n  uncached    it is not, which for most of a library is ordinary rather than wrong\n  unmeasured  no trustworthy measurement, so no cue points and no level decided before air\n  benched     every copy written off, which is the one state that means it CANNOT air\n  failing     a fetch has failed and is backing off. Not benched yet, and often the state before it"
        TrackSort:
            type: string
            enum: [title, artist, album, year, duration, rating]
            description: "What a track list is ordered BY. Its own enum for `TrackQuery`'s own reason: none of these keys\nmeans anything about an artist, and `name` is spelled `title` on a song.\n\n`state` is deliberately absent. It is three independent booleans rather than one column, so there\nis no ordering of it an operator would agree with: a benched record and an unmeasured one are not\nmore or less than each other."
        TrackQuery:
            allOf:
                - '$ref': '#/components/schemas/CatalogQuery'
                - type: object
                  properties:
                      state:
                          '$ref': '#/components/schemas/TrackState'
                      sortBy:
                          '$ref': '#/components/schemas/TrackSort'
        TrackStateCounts:
            type: object
            properties:
                total:
                    type: integer
                    minimum: 0
                    readOnly: true
                cached:
                    type: integer
                    minimum: 0
                    readOnly: true
                measured:
                    type: integer
                    minimum: 0
                    readOnly: true
                enriched:
                    type: integer
                    minimum: 0
                    readOnly: true
                benched:
                    type: integer
                    minimum: 0
                    readOnly: true
                failing:
                    type: integer
                    minimum: 0
                    readOnly: true
            required: [total, cached, measured, enriched, benched, failing]
            description: "How much of the library is in each state, over the whole filtered set rather than this page.\n\nThe aggregate is what an operator reads first — \"13 of 581 measured\" is the sentence that made\n`docs/todo/analysis-queue-ordering.md` necessary, and it was a psql query then. `total` is the\nsame number as `meta.total` when nothing is filtered, and is repeated here so the counts can be\nread as N of M without reaching into the pager."
        TrackRow:
            allOf:
                - '$ref': '#/components/schemas/Track'
                - type: object
                  properties:
                      hasAudio:
                          type: boolean
                          readOnly: true
                          description: 'The bytes are on this machine'
                      measured:
                          type: boolean
                          readOnly: true
                          description: 'Measured, COMPLETE, and at a schema version the station still trusts'
                      enriched:
                          type: boolean
                          readOnly: true
                          description: 'At least one provider has answered about it'
                  required: [hasAudio, measured, enriched]
        ArtistPage:
            type: object
            properties:
                meta:
                    '$ref': '#/components/schemas/Pagination'
                data:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Artist'
            required: [meta, data]
            description: 'One page of artists, with the totals the request was counted against'
        AlbumPage:
            type: object
            properties:
                meta:
                    '$ref': '#/components/schemas/Pagination'
                data:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Album'
            required: [meta, data]
            description: 'One page of albums'
        TrackPage:
            type: object
            properties:
                meta:
                    '$ref': '#/components/schemas/Pagination'
                data:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TrackRow'
                states:
                    '$ref': '#/components/schemas/TrackStateCounts'
            required: [meta, data, states]
            description: 'One page of tracks, with what the station has of each and of the whole set'
        EnrichmentExternalId:
            type: object
            properties:
                source:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'e.g. `musicbrainz`, `wikidata`'
                id:
                    type: string
                    minLength: 1
                    maxLength: 200
            required: [source, id]
        EnrichmentLink:
            type: object
            properties:
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                url:
                    type: string
                    minLength: 1
                    maxLength: 2000
            required: [label, url]
            description: "Narrowed to http(s) by the host before it is stored, since the console renders these as\nsomething a human clicks."
        TrackEnrichmentData:
            type: object
            properties:
                artist:
                    type: string
                    maxLength: 2000
                title:
                    type: string
                    maxLength: 2000
                album:
                    type: string
                    maxLength: 2000
                year:
                    type: integer
                releaseDate:
                    type: string
                    maxLength: 10
                genres:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                moods:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                biography:
                    type: string
                    maxLength: 20000
                facts:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                    description: 'Short lines, each independently speakable'
                bpm:
                    type: number
                    description: 'Not an integer: a tempo a source measured rather than declared is fractional'
                musicalKey:
                    type: string
                    maxLength: 2000
                label:
                    type: string
                    maxLength: 2000
                isrc:
                    type: string
                    maxLength: 2000
                artworkUrl:
                    type: string
                    maxLength: 2000
                externalIds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentExternalId'
                links:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentLink'
                extra:
                    type: object
                    additionalProperties: {}
                    description: 'What the plugin said that the SDK has no field for. Per provider only: the merged view drops it'
            description: "`releaseDate` is a string and not `datetime` because it is a partial date: MusicBrainz answers\n`1997`, `1997-06` or `1997-06-24` depending on what is actually known about the release, and the\nSDK types it the same way. A `datetime` would reject the first two or invent a day and a time\nfor them, which is a precision the source never claimed."
        ArtistEnrichmentData:
            type: object
            properties:
                name:
                    type: string
                    maxLength: 2000
                biography:
                    type: string
                    maxLength: 20000
                imageUrl:
                    type: string
                    maxLength: 2000
                genres:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                facts:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                externalIds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentExternalId'
                links:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentLink'
                extra:
                    type: object
                    additionalProperties: {}
        AlbumEnrichmentData:
            type: object
            properties:
                name:
                    type: string
                    maxLength: 2000
                artist:
                    type: string
                    maxLength: 2000
                    description: "The record's own credit, which is not always the track's"
                year:
                    type: integer
                releaseDate:
                    type: string
                    maxLength: 10
                    description: 'Partial, exactly as on TrackEnrichmentData'
                label:
                    type: string
                    maxLength: 2000
                genres:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                facts:
                    type: array
                    items:
                        type: string
                        maxLength: 2000
                artworkUrl:
                    type: string
                    maxLength: 2000
                externalIds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentExternalId'
                links:
                    type: array
                    items:
                        '$ref': '#/components/schemas/EnrichmentLink'
                extra:
                    type: object
                    additionalProperties: {}
        TrackEnrichmentSource:
            type: object
            properties:
                provider:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                providerRef:
                    type: string
                    maxLength: 200
                    readOnly: true
                    description: 'The id it was fetched under. Provenance, not identity'
                fetchedAt:
                    type: string
                    format: date-time
                    readOnly: true
                expiresAt:
                    type: string
                    format: date-time
                    readOnly: true
                stale:
                    type: boolean
                    readOnly: true
                    description: 'Past its TTL, so the next pass will ask again'
                found:
                    type: boolean
                    readOnly: true
                failed:
                    type: boolean
                    readOnly: true
                    description: 'The last attempt errored, so `expiresAt` is a backoff rather than a TTL'
                data:
                    '$ref': '#/components/schemas/TrackEnrichmentData'
            required: [provider, fetchedAt, stale, found, failed, data]
            description: "One provider's stored answer. `found: false` is a recorded miss, which is a fact rather than a\nfailure: the provider was asked, had nothing, and is not asked again until `expiresAt`. A provider\nthat could not be asked at all is `failed` instead, and the two never both hold."
        ArtistEnrichmentSource:
            type: object
            properties:
                provider:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                providerRef:
                    type: string
                    maxLength: 200
                    readOnly: true
                fetchedAt:
                    type: string
                    format: date-time
                    readOnly: true
                expiresAt:
                    type: string
                    format: date-time
                    readOnly: true
                stale:
                    type: boolean
                    readOnly: true
                found:
                    type: boolean
                    readOnly: true
                failed:
                    type: boolean
                    readOnly: true
                    description: 'The last attempt errored, so `expiresAt` is a backoff rather than a TTL'
                data:
                    '$ref': '#/components/schemas/ArtistEnrichmentData'
            required: [provider, fetchedAt, stale, found, failed, data]
        AlbumEnrichmentSource:
            type: object
            properties:
                provider:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                providerRef:
                    type: string
                    maxLength: 200
                    readOnly: true
                fetchedAt:
                    type: string
                    format: date-time
                    readOnly: true
                expiresAt:
                    type: string
                    format: date-time
                    readOnly: true
                stale:
                    type: boolean
                    readOnly: true
                found:
                    type: boolean
                    readOnly: true
                failed:
                    type: boolean
                    readOnly: true
                    description: 'The last attempt errored, so `expiresAt` is a backoff rather than a TTL'
                data:
                    '$ref': '#/components/schemas/AlbumEnrichmentData'
            required: [provider, fetchedAt, stale, found, failed, data]
        FactClaim:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                claim:
                    type: string
                    minLength: 1
                    maxLength: 500
                    readOnly: true
                    description: 'One sentence, as the DJ would say it'
                category:
                    type: string
                    minLength: 1
                    maxLength: 40
                    readOnly: true
                source:
                    type: string
                    minLength: 1
                    maxLength: 20
                    readOnly: true
                    description: "`lead` for the article's own opening, `model` for what a model found"
                sourceProvider:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                sourceUrl:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    readOnly: true
                sourceQuote:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    readOnly: true
                confidence:
                    type: number
                    readOnly: true
                model:
                    type: string
                    maxLength: 200
                    readOnly: true
                lastUsedAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'Absent means never said on air'
            required: [id, claim, category, source, sourceProvider, sourceUrl, sourceQuote]
            description: "One thing the station believes, and the words it read that say so. Extracted by the host out of\nan article a plugin handed over, rather than said by any plugin: `sourceUrl` is where a person\nchecks it and `sourceQuote` is the span that supports it, and neither is ever absent."
        TrackEnrichmentDetail:
            type: object
            properties:
                trackId:
                    type: string
                    format: uuid
                    readOnly: true
                merged:
                    '$ref': '#/components/schemas/TrackEnrichmentData'
                sources:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TrackEnrichmentSource'
                claims:
                    type: array
                    items:
                        '$ref': '#/components/schemas/FactClaim'
            required: [trackId, merged, sources, claims]
            description: "Every provider's answer, plus the same merge the promotion step used, so the console and the\ncanonical columns cannot tell different stories. `sources` is empty on a row the walk has not\nreached yet.\n\n`claims` sits beside them rather than inside `merged`, because a claim is the host's own and not\nany provider's. The articles they were read out of are deliberately NOT here: raw source prose is\nstored and never sent."
        ArtistEnrichmentDetail:
            type: object
            properties:
                artistId:
                    type: string
                    format: uuid
                    readOnly: true
                merged:
                    '$ref': '#/components/schemas/ArtistEnrichmentData'
                sources:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ArtistEnrichmentSource'
                claims:
                    type: array
                    items:
                        '$ref': '#/components/schemas/FactClaim'
            required: [artistId, merged, sources, claims]
        AlbumEnrichmentDetail:
            type: object
            properties:
                albumId:
                    type: string
                    format: uuid
                    readOnly: true
                merged:
                    '$ref': '#/components/schemas/AlbumEnrichmentData'
                sources:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AlbumEnrichmentSource'
                claims:
                    type: array
                    items:
                        '$ref': '#/components/schemas/FactClaim'
            required: [albumId, merged, sources, claims]
        StationChart:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: "Unique across the station: the plugin's own id for the chart, qualified with the plugin that offered it. Two services both calling something `top-100` stay distinct"
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                country:
                    type: string
                    maxLength: 10
                    description: 'ISO 3166-1 alpha-2, when the chart is national. Absent means global'
                genre:
                    type: string
                    maxLength: 200
                description:
                    type: string
                    maxLength: 2000
            required: [id, pluginId, name]
            description: 'A chart one installed plugin offers'
        StationChartList:
            type: object
            properties:
                charts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StationChart'
            required: [charts]
        ChartRecord:
            type: object
            properties:
                rank:
                    type: integer
                    minimum: 1
                title:
                    type: string
                    minLength: 1
                    maxLength: 400
                artist:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The lead artist alone. The other credits are in `featuring`'
                featuring:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                album:
                    type: string
                    maxLength: 400
                year:
                    type: integer
                    minimum: 0
                peak:
                    type: integer
                    minimum: 1
                    description: 'Best position this record has reached, where the source tracks it'
                weeksOn:
                    type: integer
                    minimum: 0
                    description: 'How many editions it has appeared in, where the source tracks it'
            required: [rank, title, artist]
            description: "One record's place in a chart"
        ChartQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                date:
                    type: string
                    maxLength: 10
                    description: 'Which edition, as `YYYY-MM-DD`. Absent means the current one, and a service that keeps no history answers with the current one either way'
        ChartPage:
            type: object
            properties:
                chartId:
                    type: string
                    minLength: 1
                    maxLength: 400
                records:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ChartRecord'
                    description: 'Ranked. Empty when the chart could not be read, which is deliberately not an error: a chart is something to look at, never something the station needs to air'
            required: [chartId, records]
        ClockBand:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which sort of break this slot wants, as `segments.kind` spells it. Free text: a station that wants sponsor spots writes `sponsor` and drops the recordings in'
                at:
                    type: string
                    enum: [clock, interval]
                    description: "`clock` is a time of day and `interval` is a spacing rule for a kind the station's own interval does not cover"
                hour:
                    type: integer
                    minimum: 0
                    maximum: 23
                    description: 'For a `clock` band: the hour it happens at. Absent means every hour, which is the common case'
                minute:
                    type: integer
                    minimum: 0
                    maximum: 59
                    description: 'For a `clock` band: minutes past the hour'
                everyMs:
                    type: integer
                    minimum: 60000
                    description: 'For an `interval` band: how far apart, in milliseconds'
                position:
                    type: integer
                    minimum: 0
                    description: "Where this sits in the operator's own order, which is what settles a boundary two rules both want"
                enabled:
                    type: boolean
                    description: 'A rule turned off without being lost'
                topicId:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'What this band is about, as a subject of its own kind: a news category, later a weather location. Absent means it covers whatever it finds'
                topicLabel:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                    description: "That subject's name, so a list can be drawn without a second call"
            required: [id, kind, at, position, enabled]
            description: "One rule on the station's format clock: a sort of break, and when it happens"
        ClockBandList:
            type: object
            properties:
                bands:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ClockBand'
                producibleKinds:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 100
                    description: 'Which sorts of break this station can actually make right now: one it can write and speak, one it has recordings of, or one it produces as an episode. A band naming anything else claims its boundary and then passes over it'
            required: [bands, producibleKinds]
        StationMode:
            type: string
            enum: [rotation, setlist, feature]
            description: 'What kind of programming the station is running, which decides the rules it runs under'
        AirMode:
            type: string
            enum: [audience, always]
            description: 'What the mount lease is renewed against: `audience` airs only while somebody is listening, `always` airs whenever there is a programme'
        StationAir:
            type: object
            properties:
                active:
                    type: boolean
                    description: 'False means the station was stood down. What it was playing is remembered so the console can still say what it was'
                airMode:
                    '$ref': '#/components/schemas/AirMode'
                    description: 'What puts the station on air. In `audience` mode a station that is active with a full running order is still silent while nobody is connected, which is the intended state and not a fault'
                name:
                    type: string
                    maxLength: 200
                    description: 'What is on. Absent before the station has ever been given anything to play'
                source:
                    type: string
                    maxLength: 50
                    description: 'Who built what is on: `import` or `director`'
                remaining:
                    type: integer
                    minimum: 0
                    description: 'Items left before the running order runs out and `onEnd` decides what happens'
                slotId:
                    type: string
                    maxLength: 100
                    description: 'Which slot of the schedule this broadcast belongs to. Absent means nothing scheduled it, which is every station with no schedule'
                airSource:
                    '$ref': '#/components/schemas/AirSource'
                    description: 'Who is driving the station right now'
                held:
                    type: boolean
                    description: 'Whether the schedule has been told to leave this broadcast alone. A takeover is otherwise replaced when the block it started inside ends'
                holdUntil:
                    type: string
                    minLength: 24
                    maxLength: 24
                    description: 'When that hold lapses, as an ISO-8601 instant. ABSENT WHILE `held` IS TRUE means until it is released by hand, which is a real state rather than a missing value — `Infinity` is not a thing JSON can carry, so the two facts are two fields'
            required: [active, airMode, remaining, airSource, held]
            description: 'What the station is airing, and whether it is driving at all'
        AirSource:
            type: string
            enum: ['off', schedule, sustaining, operator]
            description: 'Who chose what is on air. `schedule` is a block the clock changed over to and `sustaining` is what it plays in the hours no block claims — both are the schedule driving. `operator` is a person, including one who took over inside a scheduled block, and it holds until the next block begins. `off` is a station stood down'
        HoldStationInput:
            type: object
            properties:
                minutes:
                    type: integer
                    minimum: 1
                    maximum: 1440
                    description: 'How long the hold lasts, from now. ABSENT means until it is released by hand, which is the answer for an operator who does not know yet — a day is the ceiling because a hold nobody remembers setting is worse than one that lapses'
            description: 'How long to keep the schedule off the running order'
        SetStationAirInput:
            type: object
            properties:
                airMode:
                    '$ref': '#/components/schemas/AirMode'
            required: [airMode]
            description: 'Change how the station decides to be on air'
        StationOnEnd:
            type: string
            enum: [extend, repeat, stop]
            description: 'What the station does when the running order runs out'
        StationItemState:
            type: string
            enum: [planned, handed, airing, played, skipped, unavailable, removed]
            description: 'Where an item of the running order has got to. `handed` is a promise and `airing` is a fact, which is the distinction everything here is built around. The three terminal states that are not `played` are three different facts on a page that has to say why the station is silent: `skipped` is the station passing over an item it reached, `removed` is an operator taking one out before its turn, and `unavailable` is a record the station could not obtain the audio for — the only one of the three an operator can act on, since it names a copy rather than a decision'
        StationOrderItem:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'What an edit names, what rides through the player, and what comes back on its readings'
                kind:
                    type: string
                    enum: [track, segment]
                    description: 'Whether this is a record or something the station says: an ident, a stinger, a talk break'
                state:
                    '$ref': '#/components/schemas/StationItemState'
                title:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: "The record's title, or the segment's label. What the mount is labelled with while it airs"
                artists:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    description: 'Empty for a segment, which has no artist'
                durationMs:
                    type: integer
                    minimum: 0
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Absent on a segment: the station serves its own audio'
                externalId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'Absent on a segment'
                album:
                    type: string
                    maxLength: 400
                artworkUrl:
                    type: string
                    maxLength: 2000
                year:
                    type: integer
                    minimum: 0
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The canonical catalog track, when this is one the catalog holds'
                artistId:
                    type: string
                    maxLength: 100
                    description: 'The canonical artist behind that track, so a console can reach their page from the running order. Absent on a segment, and on a record the catalog has never seen'
                albumId:
                    type: string
                    maxLength: 100
                    description: 'The release that track was ingested inside. Absent for the two reasons above and for a third: a single ingested outside any release has none'
                rating:
                    '$ref': '#/components/schemas/Rating'
                    description: 'What the station thinks of this record, read as the order is drawn rather than stored on it. Absent on a segment, and on a record the catalog has never seen'
                segmentId:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which segment this plays. Present only on a segment'
                segmentState:
                    type: string
                    enum: [planned, writing, written, rendering, ready, failed, gone]
                playable:
                    type: boolean
                    description: 'Whether the station can actually air this segment. One that cannot is SKIPPED when it comes round, rather than held open'
                segmentError:
                    type: string
                    maxLength: 2000
                    description: 'Why this segment will not air, in a sentence. Present only on a failed one'
                segmentWriter:
                    type: string
                    maxLength: 200
                    description: "What decided the words: the station's own templates, or the model that wrote them. Absent on a recording somebody made"
                overAtMs:
                    type: integer
                    minimum: 0
                    description: 'Heard OVER the record that follows, this far into it, with the music ducked under it. Such an item is never handed to the player in its own right'
            required: [id, kind, state, title, artists]
            description: 'One item of the live running order, and where it has got to'
        StationOrder:
            type: object
            properties:
                name:
                    type: string
                    maxLength: 200
                    description: 'What is on, for a console to draw. A label for this broadcast rather than the name of a stored object'
                brief:
                    type: string
                    maxLength: 500
                    description: 'What the operator asked the station to play, in their own words. It keeps steering every refill until the station is put on air again, so a console should show it rather than only accept it'
                personaId:
                    type: string
                    maxLength: 100
                    description: 'Who is hosting this broadcast, when it named somebody. Absent means whichever persona the station has on air'
                personaLabel:
                    type: string
                    maxLength: 200
                    description: 'What that host is called, resolved as the order is read so a console need not fetch the persona list to draw a name'
                mode:
                    '$ref': '#/components/schemas/StationMode'
                onEnd:
                    '$ref': '#/components/schemas/StationOnEnd'
                source:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'Who built it: `import`, `chart` or `director`'
                sourcePluginId:
                    type: string
                    maxLength: 200
                    description: 'Where more material is pulled from, when it came from a playlist'
                sourcePlaylistId:
                    type: string
                    maxLength: 400
                sourceChartId:
                    type: string
                    maxLength: 400
                    description: 'The published chart this broadcast was built from, qualified with the plugin that offered it. Provenance rather than a binding: a chart is a fixed document, so it is read once and never topped up from'
                items:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StationOrderItem'
            required: [name, mode, onEnd, source, items]
            description: "The station's live running order: what is airing, item by item"
        PutOnAirInput:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The plugin whose playlist to build from. Absent starts empty and lets the station generate its own programming'
                playlistId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'Required alongside `pluginId`. The playlist is READ at this moment rather than copied, so it is never edited by having been aired'
                chartId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'A published chart to build from instead, as `pluginId:chartId`. An ALTERNATIVE to `pluginId` and `playlistId` rather than a companion: a chart names records where a playlist names copies, so its entries are looked up and ingested before they can air, and a station with `rotation.discover` off can play almost none of one'
                chartOrder:
                    type: string
                    enum: [countdown, ranked, unordered]
                    description: "Which way round to play it. `countdown` opens on the lowest rank and ends on number one, which is the shape a chart show has; `ranked` walks the published document from the top; `unordered` leaves the sequence to the station's own artist spacing. Absent is `countdown`. Ignored without `chartId`"
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What to call this broadcast. Absent names it after the chart, or after the plugin, since only the surface that listed the source knows its own name for it'
                brief:
                    type: string
                    maxLength: 500
                    description: 'What the station should play, in your own words: "heavy metal hits". It steers every refill for as long as this broadcast runs, not just the first batch, and it needs a model to programme with. Absent programmes the station the way its own rules do'
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Who is hosting this broadcast. It rides the running order for as long as the broadcast does, so the presenter cannot drift back mid-show. Absent uses whichever persona the station has on air'
                eraFrom:
                    type: integer
                    minimum: 1900
                    maximum: 2100
                    description: 'The earliest release year this broadcast plays. Absent means no lower bound, and a record whose year the catalog does not know is played whatever the period'
                eraTo:
                    type: integer
                    minimum: 1900
                    maximum: 2100
                    description: 'The latest release year, on the same terms. Set with `eraFrom` for a decade; either may stand alone'
                callins:
                    type: boolean
                    description: "Whether somebody phones in during this broadcast. A call is a short programme rather than a break: a few turns in a few voices, entering the running order as one block, spaced by `rotation.callinEveryMinutes`. Absent takes the station's own setting, which is off"
                mode:
                    '$ref': '#/components/schemas/StationMode'
                onEnd:
                    '$ref': '#/components/schemas/StationOnEnd'
            description: 'Put the station on air, building its running order from the top'
        SetStationHostInput:
            type: object
            properties:
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Who hosts it from here on. Absent hands it back to whichever persona the station has on air, which is what a broadcast that never named one already does'
            description: 'Change who is presenting the broadcast that is on air'
        AddStationSegmentInput:
            type: object
            properties:
                segmentId:
                    type: string
                    minLength: 1
                    maxLength: 100
                atIndex:
                    type: integer
                    minimum: 0
                    description: 'Where to put it. Absent puts it at the end. A position already handed to the player is refused'
                overAtMs:
                    type: integer
                    minimum: 0
                    maximum: 600000
                    description: 'Play it OVER the record that follows, this far into it, rather than in the gap before it. Absent plays it between two records, which is the simpler path'
            required: [segmentId]
            description: 'Put something the station says into the running order'
        AddStationTrackInput:
            type: object
            properties:
                trackId:
                    type: string
                    format: uuid
                atIndex:
                    type: integer
                    minimum: 0
                    description: 'Where to put it. Absent puts it at the end. A position already handed to the player is refused'
            required: [trackId]
            description: 'Put a catalog record into the running order. Refused at the door — 404 for a record the catalog does not hold, 422 for one whose audio is not local yet — rather than accepted and left to fail when it comes round'
        MoveStationItemInput:
            type: object
            properties:
                toIndex:
                    type: integer
                    minimum: 0
            required: [toIndex]
            description: 'Move an item within the running order'
        ExtendStationInput:
            type: object
            properties:
                count:
                    type: integer
                    minimum: 1
                    maximum: 100
            description: 'Add tracks to the running order now, rather than waiting for it to run short'
        ReplanStationInput:
            type: object
            properties:
                count:
                    type: integer
                    minimum: 1
                    maximum: 100
                    description: 'How many records to programme. Absent is roughly an hour'
                brief:
                    type: string
                    maxLength: 500
                    description: "What the station should play from here on, in your own words. Absent keeps whatever this broadcast was already asked for; an empty string CLEARS it, which hands the programming back to the station's ordinary rotation. It steers every later refill too, not just this one batch"
            description: 'Throw away everything the player is not already holding and programme it again. Unlike a shuffle, the records themselves change; unlike putting the station on air, the broadcast continues'
        HistoryEntry:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Unique across the history, and half of the cursor below'
                airedAt:
                    type: string
                    format: date-time
                    description: 'When it started, written when it began rather than when it was handed to the player'
                title:
                    type: string
                    minLength: 1
                    maxLength: 500
                artists:
                    type: string
                    minLength: 1
                    maxLength: 1000
                    description: 'The credit as written, whole: one line rather than a list, because that is the shape a release credits itself in and splitting it renames acts with a comma in their name'
                album:
                    type: string
                    maxLength: 500
                    description: 'Absent for anything aired straight from a provider, which the catalog holds no record for'
                artworkUrl:
                    type: string
                    maxLength: 2000
                    description: "The station's own copy where it has one, as a path under the API root, and the upstream URL until then. Resolve it against the base the station is reached at"
                durationMs:
                    type: integer
                    minimum: 0
                    description: 'How long the recording runs, from the catalog rather than from the copy that played'
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The catalog track this was, for a client that wants to ask more about it. Absent for a record the catalog does not hold, and for one it has since forgotten'
            required: [id, airedAt, title, artists]
            description: 'One record the station actually played'
        HistoryQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 200
                before:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Where the previous page ended. Opaque, and a keyset rather than an offset because rows arrive at the head continuously: an offset would re-show a row on every page as the station kept playing under it. Pass back whatever `nextBefore` said and nothing else'
            description: 'One page of the history, newest first'
        HistoryPage:
            type: object
            properties:
                entries:
                    type: array
                    items:
                        '$ref': '#/components/schemas/HistoryEntry'
                nextBefore:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The cursor for the page after this one, absent once the history has been read to its end'
            required: [entries]
        StationFeed:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: "Unique across the station: the plugin's own id for the feed, qualified with the plugin that offered it. Two services both calling something `world` stay distinct"
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                category:
                    type: string
                    maxLength: 200
                    description: "Broad subject, in the publisher's own word for it"
                language:
                    type: string
                    maxLength: 10
                    description: 'ISO 639-1, when the plugin knows'
                description:
                    type: string
                    maxLength: 2000
            required: [id, pluginId, name]
            description: 'A feed one installed plugin offers'
        StationFeedList:
            type: object
            properties:
                feeds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StationFeed'
            required: [feeds]
        NewsStory:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 600
                    description: 'Stable for the same entry across calls, which is what lets a reader tell an arrival from something it has already seen'
                feedId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'Qualified, matching `StationFeed.id`'
                feedName:
                    type: string
                    minLength: 1
                    maxLength: 200
                title:
                    type: string
                    minLength: 1
                    maxLength: 600
                summary:
                    type: string
                    maxLength: 2000
                    description: "The publisher's own teaser, as plain text. Never markup: this is written to be read out"
                content:
                    type: string
                    maxLength: 4000
                    description: "The story itself, as the publisher's own paragraphs. Absent when the plugin could not read one, which is ordinary: an entry that links to audio, or a page nothing could be extracted from"
                url:
                    type: string
                    maxLength: 2000
                publishedAt:
                    type: string
                    maxLength: 40
                    description: ISO-8601
                categories:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
            required: [id, feedId, feedName, title]
            description: 'One published entry'
        NewsQuery:
            type: object
            properties:
                feedId:
                    type: string
                    maxLength: 400
                    description: 'One feed, or absent for every feed the station can see, merged newest first'
                limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                since:
                    type: string
                    maxLength: 40
                    description: 'Only entries published after this ISO-8601 instant'
                headlinesOnly:
                    type: boolean
                    description: "Answer with headlines and teasers alone, skipping the story behind each one. A story is read from the publisher's own page, which is by far the slowest thing this route does, so a caller that will not use `content` should say so"
        NewsPage:
            type: object
            properties:
                stories:
                    type: array
                    items:
                        '$ref': '#/components/schemas/NewsStory'
                    description: 'Newest first. Empty when nothing could be read, which is deliberately not an error: the news is something the station may talk about, never something it needs to air'
            required: [stories]
        NowPlayingTrack:
            type: object
            properties:
                title:
                    type: string
                    minLength: 1
                    maxLength: 400
                artist:
                    type: string
                    maxLength: 400
                    description: 'Comma-joined, as a display line rather than a list: this is what a player or a device shows, not something to iterate'
                album:
                    type: string
                    maxLength: 400
                artworkUrl:
                    type: string
                    maxLength: 2000
                    description: "The station's own cached cover where there is one, the provider's URL otherwise. Relative values are paths under the API root"
                durationMs:
                    type: integer
                    minimum: 0
                startedAt:
                    type: integer
                    minimum: 0
                    description: 'Unix epoch millis, as observed when the player reported the track started'
                remainingMs:
                    type: integer
                    minimum: 0
                    description: 'Absent when the decoder cannot say. It leads the listener by the encoder and client buffers, so it is a display value and not a schedule'
            required: [title, artist, startedAt]
            description: 'The track a listener is hearing right now'
        NowPlayingMount:
            type: object
            properties:
                format:
                    type: string
                    enum: [mp3, opus, aac, flac, hls]
                    description: '`hls` is the master playlist rather than an Icecast mount, which is why this enum has an arm `PlayoutMount` does not'
                path:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Same-origin path, leading slash included. A path and not a URL: the station is reached through whatever edge served this answer, never at the address the app itself uses'
                bitrateKbps:
                    type: integer
                    minimum: 1
                    description: "Absent for FLAC, which is lossless and has no rate to set, and for HLS, whose rate is the AAC variant's"
            required: [format, path]
            description: 'One way to listen to this station right now'
        NowPlaying:
            type: object
            properties:
                station:
                    type: string
                    maxLength: 200
                    description: "The station's on-air name"
                onAir:
                    type: boolean
                    description: 'False means the station is not broadcasting. `track` is absent in that case, which is an ordinary state and not an error'
                listeners:
                    type: integer
                    minimum: 0
                    description: "How many people are listening right now. Public because it says only what Icecast's own status document already says to anyone who asks, and a station page should not need a session to show it"
                mounts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/NowPlayingMount'
                    description: 'Every way to listen, MP3 first. Never empty: MP3 has no switch. A format the operator has not switched on is ABSENT rather than present and disabled, because a client asking this wants the mounts that are actually there — and a client that had to find out by connecting to each one would put an audience-gated station on air to do it'
                track:
                    '$ref': '#/components/schemas/NowPlayingTrack'
            required: [station, onAir, listeners, mounts]
            description: 'What the station is playing, for anything that wants to display it'
        OnboardingRequirementKey:
            type: string
            enum: [admin.account]
        CoreOnboardingRequirement:
            type: object
            properties:
                key:
                    '$ref': '#/components/schemas/OnboardingRequirementKey'
                    description: 'The key of the requirement'
                title:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                    description: 'Human-readable label for the onboarding checklist'
                description:
                    type: string
                    readOnly: true
                    description: 'Optional longer explanation'
                optional:
                    type: boolean
                    readOnly: true
                    description: 'Whether the requirement is optional for onboarding'
            required: [key, title, optional]
            description: 'A single onboarding requirement'
        AdminAccountOnboardingRequirement:
            allOf:
                - '$ref': '#/components/schemas/CoreOnboardingRequirement'
                - type: object
                  properties:
                      key:
                          const: admin.account
                      value:
                          type: object
                          properties:
                              email:
                                  type: string
                                  format: email
                              password:
                                  type: string
                                  minLength: 8
                                  maxLength: 256
                                  description: 'The password'
                          required: [email, password]
                          writeOnly: true
                  required: [key, value]
        OnboardingRequirement:
            '$ref': '#/components/schemas/AdminAccountOnboardingRequirement'
        Persona:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                key:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'A stable slug, unique per station. What a log line names and what a seeded persona is recognised by'
                kind:
                    type: string
                    enum: [host, caller]
                    description: "What this character is FOR. `host` is the station's own voice; a `caller` phones in to a production, is cast per programme, and can never be put on air. Absent means `host`, so a form written before callers existed still means what it meant"
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                style:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: 'Completes "You are …". Who they ARE; the sheet below carries how they talk'
                djName:
                    type: string
                    maxLength: 200
                    description: "The name this character goes by on air, overriding the station's own while it is active"
                voice:
                    type: string
                    maxLength: 200
                    description: "The station voice that speaks this persona, as the id a speech plugin maps. Empty means that plugin's default"
                soundboard:
                    type: string
                    maxLength: 200
                    description: 'The soundboard this character has to hand, as the name of a board in the pad library. Empty for a presenter who works without one. What reaches a model is the names of the pads on that board, never this word'
                diction:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                    description: 'The dialect: grammar and substitutions that apply to every sentence rather than to a subject'
                dictionMarkers:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 100
                    description: "Words whose presence proves the dialect survived. What a model's answer is checked against"
                quirks:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                    description: 'What they always and never do on air'
                preoccupations:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                    description: 'The standing subjects they keep coming back to. Exactly ONE reaches any one break, chosen by rotation, which is what makes a character sound like it has things on its mind rather than one thing'
                catchphrases:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    description: 'Signature phrases, asked for sparingly'
                avoid:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    description: 'Wording that breaks the character'
                background:
                    type: string
                    maxLength: 2000
                    description: 'A couple of grounded facts they may self-reference'
                brevity:
                    type: string
                    enum: [short, one-line]
                    description: "How much this character says. Absent for the station's ordinary length; the rung above it is `latitude`, which is a different kind of thing rather than a longer one"
                latitude:
                    type: string
                    enum: [loose, unleashed]
                    description: "How much room this character is given, above the station's ordinary discipline: a bigger word ceiling, a licence to follow the thought instead of making one point, and at `unleashed` no restraint on how it says it. Offered only by the ordinary talk break, always outranked by the station's content policy, and it switches off no refusal"
                chattiness:
                    type: string
                    enum: [reserved, sparing, ordinary, chatty, relentless]
                    description: "How often this character talks, as a scale on the station's own interval between breaks. Absent is `ordinary`, which is that interval unchanged. The quietest rung is half as often and never silence: turning the station's breaks off is a station setting, and two switches for one thing can disagree"
                storytelling:
                    type: string
                    enum: [never, occasionally, often]
                    description: 'How readily this character works one of its own stories into an ordinary talk break. Absent is `occasionally`, which offers one only where the station knows nothing about the records either side. The stories themselves are their own list, and a `story` band on the clock outranks this whatever it says'
                samples:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                    description: 'Lines in their own voice, used as examples and as a console preview'
                templates:
                    type: string
                    maxLength: 20000
                    description: "This character's own break phrasings, one per line. Empty means the station's global ones"
                active:
                    type: boolean
                    readOnly: true
                    description: 'Whether this is the one on air. At most one per station'
            required: [id, key, label, style, active]
            description: 'Who the station is when it opens its mouth: the character a model writes in, the phrasings underneath it, the voice that says them, and what it plays'
        PersonaList:
            type: object
            properties:
                personas:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Persona'
            required: [personas]
        PersonaRequest:
            type: object
            properties:
                description:
                    type: string
                    minLength: 1
                    maxLength: 2000
            required: [description]
            description: "A description of a character, in the operator's own words"
        PersonaDraftView:
            type: object
            properties:
                key:
                    type: string
                    minLength: 1
                    maxLength: 100
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                style:
                    type: string
                    minLength: 1
                    maxLength: 2000
                djName:
                    type: string
                    maxLength: 200
                voice:
                    type: string
                    maxLength: 200
                soundboard:
                    type: string
                    maxLength: 200
                diction:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                dictionMarkers:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 100
                quirks:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                preoccupations:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                catchphrases:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                avoid:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                background:
                    type: string
                    maxLength: 2000
                brevity:
                    type: string
                    enum: [short, one-line]
                latitude:
                    type: string
                    enum: [loose, unleashed]
                chattiness:
                    type: string
                    enum: [reserved, sparing, ordinary, chatty, relentless]
                storytelling:
                    type: string
                    enum: [never, occasionally, often]
                samples:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                templates:
                    type: string
                    maxLength: 20000
            required: [key, label, style]
            description: "A persona as a form's contents rather than a row: no id and not on air, because nothing has been\nsaved. The console opens this in the editor and the operator saves it through POST /personas, which\nis what keeps generating a way of filling in the form rather than a second writer of the table"
        GeneratedPersona:
            type: object
            properties:
                persona:
                    '$ref': '#/components/schemas/PersonaDraftView'
                stories:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaStoryWrite'
                    description: "A couple of things that have happened to this character. Beside the form rather than in it, because they are their own table: the console saves the persona and then writes these through the stories route, so they go through the same validation an operator's own typing does"
                droppedMarkers:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 100
                    description: 'Words the model called markers that its own sample lines never used. Dropped, because the samples are the evidence and the marker list is the claim — a marker nothing says declines every break and looks exactly like a model that is switched off'
                droppedTemplates:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 500
                    description: 'Phrasings naming a value the vocabulary does not have. Dropped by the LINE, since five good phrasings and one broken one is five phrasings'
            required: [persona, stories, droppedMarkers, droppedTemplates]
            description: 'What a model wrote, and what had to be dropped to make it usable'
        PersonaNote:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                kind:
                    type: string
                    enum: [said, trait]
                    description: "`said` is what this character did, rendered beside the show's memory. `trait` is who it has become, rendered beside the sheet"
                note:
                    type: string
                    minLength: 1
                    maxLength: 500
                    description: 'One sentence, because it shares a system turn with the grounding rules'
                state:
                    type: string
                    enum: [active, suggested, rejected]
                    readOnly: true
                    description: '`active` is carried into breaks. `rejected` outlives the pass that proposed it, or the same scripts propose it again forever'
                origin:
                    type: string
                    enum: [operator, model]
                    readOnly: true
                    description: "Who says so. `model` is the distil pass reading this character's own history back"
                sourceScriptId:
                    type: string
                    maxLength: 100
                    readOnly: true
                    description: 'The attempt this was drawn from, while that row still exists. The nightly sweep takes it and the quote below stays'
                sourceQuote:
                    type: string
                    maxLength: 2000
                    readOnly: true
                    description: 'The words that support it, as the station said them. What an operator actually accepts or rejects on, and required of anything a model wrote'
                lastUsedAt:
                    type: string
                    maxLength: 40
                    readOnly: true
                    description: 'When it was last carried into a break. Absent means never, which is what puts it at the front of the rotation'
                createdAt:
                    type: string
                    minLength: 1
                    maxLength: 40
                    readOnly: true
            required: [id, kind, note, state, origin, createdAt]
            description: "One thing this character has accumulated that its sheet does not hold. Two kinds and they are two\ndifferent claims: `said` records something it actually put on air and carries the script as its\nevidence, so it is a record and goes straight into use; `trait` infers who the character is\nbecoming, which nothing can verify, so a model's arrives `suggested` and the operator is the check"
        PersonaNoteList:
            type: object
            properties:
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                notes:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaNote'
            required: [personaId, notes]
            description: "One character's whole notebook, oldest first, in every state"
        PersonaNoteWrite:
            type: object
            properties:
                kind:
                    type: string
                    enum: [said, trait]
                note:
                    type: string
                    minLength: 1
                    maxLength: 500
            required: [kind, note]
            description: 'A note an operator is writing by hand. Always active and always theirs; a proposal is something only the distil pass creates'
        PersonaNoteState:
            type: object
            properties:
                state:
                    type: string
                    enum: [active, suggested, rejected]
            required: [state]
            description: 'Accepting a proposal, turning one down, or taking a note out of use without losing it'
        PersonaStory:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                title:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'A short handle. Never spoken; what this list is read by and what a proposal names'
                story:
                    type: string
                    minLength: 1
                    maxLength: 4000
                    description: "The telling itself, in the character's voice. Already speakable, because the floor reads it as it stands"
                state:
                    type: string
                    enum: [active, suggested, rejected]
                    readOnly: true
                    description: '`active` can be told. `rejected` outlives the pass that proposed it, or the same catalogue proposes it forever'
                origin:
                    type: string
                    enum: [operator, model]
                    readOnly: true
                    description: 'Who says so. `model` is the enrichment pass writing from what the station already holds'
                source:
                    type: string
                    maxLength: 1000
                    readOnly: true
                    description: "Where a proposal came from, in the station's own words. Absent for anything an operator wrote"
                details:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaStoryDetail'
                    readOnly: true
                    description: 'What it has picked up since, in every state'
                lastToldAt:
                    type: string
                    maxLength: 40
                    readOnly: true
                    description: 'Absent means never told, which is what puts it at the front of the rotation'
                timesTold:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'How often it has gone out, which changes how the model is asked to tell it'
                createdAt:
                    type: string
                    minLength: 1
                    maxLength: 40
                    readOnly: true
            required: [id, title, story, state, origin, details, timesTold, createdAt]
            description: "Something that happened to this character, in its own telling. Not a claim about the world and never\nchecked as one: `source` says where a proposal came from, for the operator reading it, and nothing\ndownstream reads it as evidence — see `persona.story.ts` for why that is the load-bearing difference\nfrom a fact"
        PersonaStoryDetail:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
                state:
                    type: string
                    enum: [active, suggested, rejected]
                    readOnly: true
                origin:
                    type: string
                    enum: [operator, model]
                    readOnly: true
                source:
                    type: string
                    maxLength: 1000
                    readOnly: true
                createdAt:
                    type: string
                    minLength: 1
                    maxLength: 40
                    readOnly: true
            required: [id, detail, state, origin, createdAt]
            description: 'One thing a story has picked up since it was written. A row rather than a rewrite, so an invented clause can be turned down without losing the story'
        PersonaStoryList:
            type: object
            properties:
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                stories:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaStory'
            required: [personaId, stories]
            description: 'Every story one character holds, oldest first, in every state'
        PersonaStoryWrite:
            type: object
            properties:
                title:
                    type: string
                    minLength: 1
                    maxLength: 200
                story:
                    type: string
                    minLength: 1
                    maxLength: 4000
            required: [title, story]
            description: 'A story an operator is writing by hand. Always active and always theirs; a proposal is something only the enrichment pass creates'
        PersonaStoryDetailWrite:
            type: object
            properties:
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
            required: [detail]
            description: 'One thing to add to a story that already exists'
        PersonaStoryState:
            type: object
            properties:
                state:
                    type: string
                    enum: [active, suggested, rejected]
            required: [state]
            description: 'Accepting a proposal, turning one down, or taking a story out of the rotation without losing it'
        PersonaFile:
            type: object
            properties:
                format:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'What shape this is, so a file from a later build says so rather than being read wrongly. The shapes below are what an import actually validates; this is for the human reading the failure'
                takenAt:
                    type: string
                    minLength: 1
                    maxLength: 40
                    description: 'When it was exported, ISO-8601'
                station:
                    type: string
                    maxLength: 100
                    description: 'The station it was taken from. Provenance only: an import writes into whichever station it is running as, and the two need not match'
                personas:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaFilePersona'
            required: [format, takenAt, personas]
            description: "A character as a file: everything somebody would have to send to put this presenter on another\nstation, and nothing that belongs to the station it came from"
        PersonaFilePersona:
            allOf:
                - '$ref': '#/components/schemas/PersonaDraftView'
                - type: object
                  properties:
                      kind:
                          type: string
                          enum: [host, caller]
                          description: 'Absent means `host`, as everywhere else'
                      soundboard:
                          type: string
                          maxLength: 200
                          description: 'The board this character reaches for. Carried even though the receiving station may not hold it: a persona naming a rack that does not exist and one with no rack are the same state, and the import says which it got'
                      stories:
                          type: array
                          items:
                              '$ref': '#/components/schemas/PersonaFileStory'
                  required: [stories]
        PersonaFileStory:
            type: object
            properties:
                title:
                    type: string
                    minLength: 1
                    maxLength: 200
                story:
                    type: string
                    minLength: 1
                    maxLength: 4000
                state:
                    type: string
                    enum: [active, rejected]
                    description: 'Absent means `active`. A turned-down story travels so the enrichment pass does not propose it again on the far side; an undecided one does not travel at all, because nobody has decided it yet'
                details:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaFileStoryDetail'
            required: [title, story, details]
            description: "Something that happened to this character, as a file carries it. No `origin` and no `source`,\nunlike the stored row: whoever exported this stood behind every story in it, so on the far side\nthey are the receiving operator's own, and a sentence about where a proposal came from names a\ncatalogue that station does not have"
        PersonaFileStoryDetail:
            type: object
            properties:
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
                state:
                    type: string
                    enum: [active, rejected]
            required: [detail]
            description: 'One thing a story picked up after it was written, carried the same way and for the same reasons'
        PersonaImportPlan:
            type: object
            properties:
                format:
                    type: string
                    minLength: 1
                    maxLength: 50
                    readOnly: true
                    description: 'What the file said it was. Reported rather than enforced: this repo edits migrations in place, so a version stamp cannot promise a shape, and the shapes are what was actually validated'
                station:
                    type: string
                    maxLength: 100
                    readOnly: true
                    description: 'The station it was taken from, when it said'
                takenAt:
                    type: string
                    maxLength: 40
                    readOnly: true
                    description: 'When it was taken, when it said'
                notices:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaImportNotice'
                    readOnly: true
                    description: 'About the FILE rather than any one character in it'
                personas:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaImportEntry'
                    readOnly: true
            required: [format, notices, personas]
            description: "What importing a file WOULD do, worked out against this station and written nowhere.\n\nThe same code the import itself runs, so what this reports is what will happen rather than a second\nopinion about it. It answers two questions an operator cannot get from the file alone: which\ncharacters are new here and which would be rewritten, and what this station cannot honour about them"
        PersonaImportEntry:
            type: object
            properties:
                key:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                    description: 'What identifies this character across two installs'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                    readOnly: true
                kind:
                    type: string
                    enum: [host, caller]
                    readOnly: true
                outcome:
                    type: string
                    enum: [create, update]
                    readOnly: true
                    description: 'Whether this station holds a character under this key already. An update rewrites the sheet and adds stories; it never deletes one the operator here wrote'
                storiesNew:
                    type: integer
                    minimum: 0
                    readOnly: true
                storiesHeld:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Already here under the same handle, so importing would skip them'
                detailsNew:
                    type: integer
                    minimum: 0
                    readOnly: true
                detailsHeld:
                    type: integer
                    minimum: 0
                    readOnly: true
                notices:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaImportNotice'
                    readOnly: true
            required:
                - key
                - label
                - outcome
                - storiesNew
                - storiesHeld
                - detailsNew
                - detailsHeld
                - notices
            description: 'One character in a file, and what would become of it here'
        PersonaImportResult:
            type: object
            properties:
                plan:
                    '$ref': '#/components/schemas/PersonaImportPlan'
                    readOnly: true
                    description: 'What it decided to do, notices and all, so the answer carries its own explanation'
                created:
                    type: integer
                    minimum: 0
                    readOnly: true
                updated:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Characters whose sheet was rewritten. An update replaces the sheet and ADDS stories; it never deletes one the operator here wrote'
                storiesWritten:
                    type: integer
                    minimum: 0
                    readOnly: true
                detailsWritten:
                    type: integer
                    minimum: 0
                    readOnly: true
                personas:
                    '$ref': '#/components/schemas/PersonaList'
                    readOnly: true
                    description: "The roster as it now stands, on this file's own rule: every mutation answers the whole list, because more than the named row can change"
            required: [plan, created, updated, storiesWritten, detailsWritten, personas]
            description: "What importing actually did, with the plan it did it from.\n\nAll or nothing: a file whose import failed part-way leaves the station exactly as it was, on\n`PUT /settings`' own rule. The preview is what stands between an operator and a surprise, so a\npartial landing would be the one outcome nothing had described"
        PersonaImportNotice:
            type: object
            properties:
                kind:
                    type: string
                    enum: [format, duplicate, on-air, clears, voice, soundboard, phrasing, markers]
                    readOnly: true
                    description: 'Which sort, so a console can group or ignore by it rather than parsing the sentence'
                message:
                    type: string
                    minLength: 1
                    maxLength: 500
                    readOnly: true
                    description: "The whole of it, in the station's own words, because its destination is a person"
            required: [kind, message]
            description: "Something to know before pressing Import. Not a refusal: every one of these describes a state the\nstation can be in perfectly well, and the point of saying it is that each one is otherwise\ndiscovered by putting the character on air"
        PersonaRehearsalAttempt:
            type: object
            properties:
                writer:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which binding was asked, as `segments.writer` would record it'
                outcome:
                    type: string
                    minLength: 1
                    maxLength: 20
                    description: 'written, declined or failed. Declined is the station working; failed is something to go and fix'
                durationMs:
                    type: integer
                    minimum: 0
                    description: 'Kept for every writer rather than only a slow one: "the model got slower" can only be asked of numbers gathered before anybody suspected it'
                script:
                    type: string
                    maxLength: 5000
                    description: 'What it produced, when it produced anything usable'
                reason:
                    type: string
                    maxLength: 1000
                    description: 'Why it did not, when it did not. A sentence, because its destination is a person'
            required: [writer, outcome, durationMs]
            description: "One writer's turn at a rehearsal. Every writer asked is reported and not only the one that won: a\nmodel that declined and a floor that covered for it are two facts, and the second on its own reads\nas a station that never had a model configured"
        PersonaRehearsal:
            type: object
            properties:
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                previous:
                    type: string
                    minLength: 1
                    maxLength: 500
                    description: 'The invented record the break follows. Fixed, so two readings of the same sheet can be compared'
                next:
                    type: string
                    minLength: 1
                    maxLength: 500
                    description: 'The invented record it leads into'
                attempts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaRehearsalAttempt'
                script:
                    type: string
                    maxLength: 5000
                    description: 'The words a listener would have heard, from whichever writer answered first'
                writer:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which one that was. Present exactly when `script` is'
                reason:
                    type: string
                    maxLength: 1000
                    description: 'Why there are no words, when every writer had nothing. Not a fault: a break nothing could write is one the station does not take'
            required: [personaId, previous, next, attempts]
            description: 'What a persona says when it is asked for a break it will never air'
        PersonaAuditionRequest:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Which catalog plugin the playlist belongs to'
                playlistId:
                    type: string
                    minLength: 1
                    maxLength: 400
                name:
                    type: string
                    maxLength: 400
                    description: 'What the playlist is called, kept as a caption for the run. The console already holds it, and a run whose playlist is later renamed or deleted stays readable'
                limit:
                    type: integer
                    minimum: 1
                    maximum: 50
                    default: 10
                    description: 'How many breaks to write. One more record than this is taken off the playlist, since a break sits between two'
            required: [pluginId, playlistId, limit]
            description: "What an operator asks for when they put a character through a playlist. The playlist is READ at the\nmoment of asking and its records are stored on the run, so a list edited at the provider afterwards\ndoes not change what was measured"
        PersonaAuditionSource:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                playlistId:
                    type: string
                    minLength: 1
                    maxLength: 400
                name:
                    type: string
                    maxLength: 400
            required: [pluginId, playlistId]
            description: "Where the records came from. A snapshot of the name rather than a reference, so a playlist renamed\nor deleted at the provider leaves a finished audition readable"
        PersonaAuditionRecord:
            type: object
            properties:
                title:
                    type: string
                    minLength: 1
                    maxLength: 500
                artist:
                    type: string
                    maxLength: 500
                    description: 'The lead, as it should be read'
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The catalog row, when the station holds this copy. Absent for a record it has never seen'
                year:
                    type: integer
                    minimum: 0
                    maximum: 3000
                album:
                    type: string
                    maxLength: 500
                durationMs:
                    type: integer
                    minimum: 0
            required: [title, artist]
            description: 'One record, exactly as the writers were shown it'
        PersonaAuditionBreak:
            type: object
            properties:
                ordinal:
                    type: integer
                    minimum: 0
                    description: 'Which transition, from 0'
                previous:
                    '$ref': '#/components/schemas/PersonaAuditionRecord'
                    description: 'The record this break follows'
                next:
                    '$ref': '#/components/schemas/PersonaAuditionRecord'
                    description: 'The one it leads into'
                attempts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaRehearsalAttempt'
                script:
                    type: string
                    maxLength: 5000
                    description: 'The words a listener would have heard, from whichever writer answered first'
                writer:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which one that was. Present exactly when `script` is'
                reason:
                    type: string
                    maxLength: 1000
                    description: 'Why there are none, when every writer had nothing. On air this break is skipped'
            required: [ordinal, previous, next, attempts]
            description: "One transition, and everything the writers said about it. Every writer asked is reported and not\nonly the one that won, on the rehearsal's own argument: a model that declined and a floor that\ncovered for it are two facts"
        PersonaAuditionSummary:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                personaId:
                    type: string
                    minLength: 1
                    maxLength: 100
                personaKey:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: "The character's own key, as `script_history` records it"
                source:
                    '$ref': '#/components/schemas/PersonaAuditionSource'
                state:
                    type: string
                    enum: [queued, running, done, failed, cancelled]
                    description: '`cancelled` keeps whatever breaks were already written'
                transitions:
                    type: integer
                    minimum: 0
                    description: 'How many breaks this run writes in total'
                written:
                    type: integer
                    minimum: 0
                    description: 'How many it has written so far, which is how far along it is'
                error:
                    type: string
                    maxLength: 2000
                    description: 'Why writing it stopped, when it did'
                cancelledAt:
                    type: string
                    maxLength: 40
                    description: ISO-8601
                finishedAt:
                    type: string
                    maxLength: 40
                    description: 'ISO-8601, whichever way the run ended'
                createdAt:
                    type: string
                    minLength: 1
                    maxLength: 40
                    description: ISO-8601
            required: [id, personaId, personaKey, source, state, transitions, written, createdAt]
            description: 'A run of one character over one playlist, without its breaks: what a list draws'
        PersonaAudition:
            allOf:
                - '$ref': '#/components/schemas/PersonaAuditionSummary'
                - type: object
                  properties:
                      breaks:
                          type: array
                          items:
                              '$ref': '#/components/schemas/PersonaAuditionBreak'
                  required: [breaks]
        PersonaAuditionList:
            type: object
            properties:
                auditions:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PersonaAuditionSummary'
            required: [auditions]
        PlaylistPermission:
            type: string
            enum: [read, edit]
            description: "An action a source will permit on one playlist's items. Item-scoped: neither value covers the playlist's own name or description"
        CatalogPlaylist:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                pluginName:
                    type: string
                    minLength: 1
                    maxLength: 200
                id:
                    type: string
                    minLength: 1
                    maxLength: 400
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                description:
                    type: string
                    maxLength: 2000
                trackCount:
                    type: integer
                    minimum: 0
                artworkUrl:
                    type: string
                    maxLength: 2000
                permissions:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PlaylistPermission'
                    description: "What the SOURCE permits on this playlist's items, not what this actor may do. Empty means the source permits nothing; absent means it did not say"
            required: [pluginId, pluginName, id, name]
            description: 'A playlist a catalog-capable plugin offers, tagged with the plugin it came from so an aggregated list is addressable'
        CatalogTrack:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: "The PROVIDER's id for this copy, which is what an import names it by. Never a `deadair.tracks` id"
                title:
                    type: string
                    minLength: 1
                    maxLength: 400
                artists:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    description: 'Ordered, primary artist first. Empty array if the provider genuinely has none'
                album:
                    type: string
                    maxLength: 400
                durationMs:
                    type: integer
                    minimum: 0
                isrc:
                    type: string
                    maxLength: 100
                artworkUrl:
                    type: string
                    maxLength: 2000
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The canonical `deadair.tracks` row this copy is bound to, when the catalog holds one'
                artistId:
                    type: string
                    maxLength: 100
                    description: 'The canonical artist behind that row'
                albumId:
                    type: string
                    maxLength: 100
                    description: 'The release that row was ingested inside. Absent for a single ingested outside any'
            required: [id, title, artists]
            description: "One record as its PROVIDER describes it, plus what the catalog can say about the same copy.\n\nThe first half mirrors the plugin SDK's `ProviderTrack` and stays the provider's answer: this is a\nlisting of what a playlist holds, not of what the station has ingested. The three ids below are the\nstation's own and are absent for anything it has never seen, which on most playlists is plenty of\nrows — a playlist is a provider's list and the library is what a sync has walked"
        CatalogSourceError:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                pluginName:
                    type: string
                    minLength: 1
                    maxLength: 200
                message:
                    type: string
                    maxLength: 4000
            required: [pluginId, pluginName, message]
            description: 'One catalog-capable plugin that could not be listed'
        CatalogPlaylistPage:
            type: object
            properties:
                playlists:
                    type: array
                    items:
                        '$ref': '#/components/schemas/CatalogPlaylist'
                errors:
                    type: array
                    items:
                        '$ref': '#/components/schemas/CatalogSourceError'
            required: [playlists, errors]
        CatalogPlaylistTracks:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                playlistId:
                    type: string
                    minLength: 1
                    maxLength: 400
                tracks:
                    type: array
                    items:
                        '$ref': '#/components/schemas/CatalogTrack'
            required: [pluginId, playlistId, tracks]
        PlayoutPlaylistInput:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                playlistId:
                    type: string
                    minLength: 1
                    maxLength: 400
            required: [pluginId, playlistId]
            description: 'The plugin playlist to load into the running order'
        PlayoutChartInput:
            type: object
            properties:
                chartId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'As `pluginId:chartId`, which is how `GET /charts` lists them'
                chartOrder:
                    type: string
                    enum: [countdown, ranked, unordered]
                    description: 'Which way round to play it. Absent is `countdown`, which opens on the lowest rank and ends on number one'
            required: [chartId]
            description: 'The published chart to build the running order from'
        PlayoutItem:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: "deadair's own id for this item, not the provider's: a playlist may hold the same track twice"
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                externalId:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: "The track's id in its plugin's id space"
                title:
                    type: string
                    minLength: 1
                    maxLength: 400
                artists:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                durationMs:
                    type: integer
                    minimum: 0
                    description: 'Integer milliseconds. Deliberately not the `duration` scalar, which is a Luxon `Duration` over an ISO-8601 string'
                album:
                    type: string
                    maxLength: 400
                artworkUrl:
                    type: string
                    maxLength: 2000
                    description: "The locally cached cover where there is one, the provider's URL otherwise"
                year:
                    type: integer
                    minimum: 0
                    description: 'First release year, when the catalog knows one'
                trackId:
                    type: string
                    maxLength: 100
                    description: 'The canonical `deadair.tracks` id, when this item is a track the catalog holds. Absent for anything the catalog has never seen'
            required: [id, pluginId, externalId, title, artists]
            description: 'One item in the running order, as the console sees it'
        PlayoutNowPlaying:
            type: object
            properties:
                item:
                    '$ref': '#/components/schemas/PlayoutItem'
                startedAt:
                    type: integer
                    minimum: 0
                    description: 'Unix epoch millis, as observed when the player reported it'
                remainingMs:
                    type: integer
                    minimum: 0
                    description: "The decoder's own countdown, absent when it cannot say. It leads the listener by the encoder and client buffers"
            required: [item, startedAt]
            description: 'What the PLAYER says is airing, which is not the same as what was last handed to it'
        StreamConfigWarning:
            type: object
            properties:
                container:
                    type: string
                    enum: [icecast, liquidsoap]
                    description: 'Which one is behind'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
                    description: 'What is wrong and how it is known, in a sentence'
                restart:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The exact command that adopts the new config, which is the only thing that does'
            required: [container, detail, restart]
            description: "A stream container still running config the app has replaced. Icecast and Liquidsoap read\ntheir rendered config ONCE, at startup, and nothing restarts or signals them when it is\nre-rendered — so a reseeded secret leaves a process holding credentials that match nothing,\nand the symptom names something else entirely (every listener refused, or no mount at all).\nThe app cannot restart a sibling container and should not be able to, so it reports."
        SilenceCause:
            type: string
            enum:
                - airing
                - transportStalled
                - controlDenied
                - streamUnreachable
                - configNotAdopted
                - stoodDown
                - noProgramme
                - noAudience
                - warmingUp
                - waitingOnAudio
                - notDriving
                - starved
            description: "Which gate is keeping the station quiet, or `airing` when none of them is. Ordered by cause: a\nstalled transport loop makes every reading under it stale, so it is ruled out first"
        SilenceState:
            type: string
            enum: [ok, waiting, fault]
            description: "How one gate is doing. `waiting` is its own state rather than a mild fault, because a station\nidling for want of a listener and a station that cannot reach its stream are both silent and only\none of them is something to go and fix"
        SilenceCheck:
            type: object
            properties:
                code:
                    '$ref': '#/components/schemas/SilenceCause'
                    description: 'Never `airing`, which is the absence of a blocking gate rather than a gate'
                state:
                    '$ref': '#/components/schemas/SilenceState'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
                    description: 'What this gate is doing right now, whether or not it is the one blocking'
                remedy:
                    type: string
                    maxLength: 500
                    description: 'What would clear it, where there is something an operator can actually do'
            required: [code, state, detail]
            description: "One gate's answer about itself"
        StationSilence:
            type: object
            properties:
                audible:
                    type: boolean
                    description: 'Whether the station believes its programme is reaching the mount. NOT whether anybody is hearing it: a station can be audible with no listeners in `always` mode, and can have listeners while airing the local bed'
                cause:
                    '$ref': '#/components/schemas/SilenceCause'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 1000
                remedy:
                    type: string
                    maxLength: 500
                checks:
                    type: array
                    items:
                        '$ref': '#/components/schemas/SilenceCheck'
                    description: 'Every gate, in the order they are judged, so a console can say what it ruled out. A `configNotAdopted` fault appears here and is never the cause, because a station can air perfectly well while it is true'
            required: [audible, cause, detail, checks]
            description: 'Why the station cannot be heard, as one answer'
        PlayoutMount:
            type: object
            properties:
                format:
                    type: string
                    enum: [mp3, opus, aac, flac]
                path:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Same-origin path, on the same terms as `PlayoutStatus.mountPath`'
                bitrateKbps:
                    type: integer
                    minimum: 1
                    description: 'Absent for FLAC, which is lossless and has no rate to set'
            required: [format, path]
            description: 'One mount the station is publishing right now'
        PlayoutStatus:
            type: object
            properties:
                streamUp:
                    type: boolean
                    description: "Whether Liquidsoap's control API is answering at all. False means nothing can air, whatever the running order holds"
                onAir:
                    type: boolean
                    description: 'Whether the station is actually broadcasting. deadair holds the mount on a lease it renews only while it has a programme, so a reachable stream with nothing to play is up and NOT on air: it is connected, and airing silence'
                mountPath:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Same-origin path of the Icecast MP3 mount, which is always published and is the one a console names when it can only name one. A path rather than a URL: the browser reaches Icecast through whatever edge served the SPA, never at the address the app itself uses'
                mounts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PlayoutMount'
                    description: 'Every mount being published, MP3 first, so a console can offer the others rather than implying the station is only on one. Never empty: MP3 has no switch. A format the operator has not switched on is absent rather than present and disabled, because every consumer of this wants the mounts that are actually there'
                nowPlaying:
                    '$ref': '#/components/schemas/PlayoutNowPlaying'
                upNext:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PlayoutItem'
                    description: 'Waiting here, in order. Excludes what the player already holds'
                queuedCount:
                    type: integer
                    minimum: 0
                    description: 'How many items are waiting in total, of which `upNext` is the head'
                listeners:
                    type: integer
                    minimum: 0
                    description: 'How many clients Icecast has attached to the mount. Zero both for "nobody is listening" and for an Icecast that is not answering, which `audience` is where to tell apart'
                audience:
                    type: boolean
                    description: 'Whether the station counts as having an audience, which lingers for a minute past the last listener so a reconnecting player does not cut the broadcast'
                staleStreamConfig:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StreamConfigWarning'
                    description: 'Containers running config the app has since replaced. Empty is the ordinary state, and so is empty for anything the app has no evidence about: a warning here has never been a guess'
                silence:
                    '$ref': '#/components/schemas/StationSilence'
                    description: 'Which gate is keeping the station quiet, composed from every one of them rather than inferred from the fields above. `streamUp`, `onAir`, `audience` and `queuedCount` each answer for one gate and a console reading them alone has to guess at the rest'
            required:
                - streamUp
                - onAir
                - mountPath
                - mounts
                - upNext
                - queuedCount
                - listeners
                - audience
                - staleStreamConfig
                - silence
            description: "The station's transport, as one reading"
        PluginStatus:
            type: string
            enum: [discovered, disabled, misconfigured, active, failed]
            description: 'Lifecycle state of a plugin the host knows about'
        ConfigFieldType:
            type: string
            enum: [string, text, url, secret, number, boolean, select, multiselect, list, note]
        ConfigFieldUnit:
            type: string
            enum: [bytes, fraction]
            description: "What a `number` field's value is measured in. The stored value is always in this unit; only the\ncontrol the operator touches changes, so a byte count stays a byte count everywhere it is read and\na `fraction` stays the share between 0 and 1 that the code multiplying by it wants"
        ConfigFieldControl:
            type: string
            enum: [slider, tags]
            description: "The control a field asks to be drawn with, where the ordinary one for its type reads badly. Opt-in\nper field rather than inferred, because a slider is right for a value you feel for and wrong for\none you have to hit exactly, and `tags` is right for a comma-separated line that is really a SET\nand wrong for one that is prose. Nothing about the stored value changes either way"
        ConfigFieldOption:
            type: object
            properties:
                value:
                    type: string
                    minLength: 1
                    maxLength: 200
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
            required: [value, label]
            description: 'One choice of a `select` config field'
        ConfigFieldOptionSource:
            type: string
            enum:
                - station.newsCategories
                - station.newsFeeds
                - intl.timeZones
                - plugins.speech
                - plugins.llm
                - plugins.mixer
                - plugins.analysis
                - llm.models
            description: "Where a field's or a column's choices come from when only the console can enumerate them: the\nstation's own tables, the platform's zone list, the enabled plugins that can do one of four jobs,\nor the models the selected model plugin currently offers. Resolved by the console either way"
        ConfigFieldColumn:
            type: object
            properties:
                key:
                    type: string
                    minLength: 1
                    maxLength: 200
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                type:
                    type: string
                    enum: [string, url, select, secret]
                required:
                    type: boolean
                placeholder:
                    type: string
                    maxLength: 400
                options:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ConfigFieldOption'
                optionsFrom:
                    '$ref': '#/components/schemas/ConfigFieldOptionSource'
                dependsOn:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: "Key of another column in the same list. This cell applies only to a row whose cell there holds one of `dependsOnValues`. Stronger than a field's `dependsOn`, which only hides a control: a cell that does not apply is neither sent by the console nor read by the host, so a `url` column that does not apply to a row contributes no hostname to the plugin's allowlist. A target this list does not declare, or a target cell still empty, shows the cell"
                dependsOnValues:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    description: 'The values of the `dependsOn` cell this one applies to. Omitted means any non-empty value; ignored without a target'
            required: [key, label, type]
            description: "One column of a `list` field. Every ordinary cell is stored as a string in the row, so this describes the\ncontrol rather than the value; a `secret` cell is encrypted on its own and is never in the row at all"
        ConfigFieldDescriptor:
            type: object
            properties:
                key:
                    type: string
                    minLength: 1
                    maxLength: 200
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                type:
                    '$ref': '#/components/schemas/ConfigFieldType'
                required:
                    type: boolean
                default:
                    oneOf:
                        - type: string
                        - type: number
                        - type: boolean
                unit:
                    '$ref': '#/components/schemas/ConfigFieldUnit'
                    description: '`number` only, and ignored elsewhere'
                control:
                    '$ref': '#/components/schemas/ConfigFieldControl'
                    description: '`slider` for a `number` with both `min` and `max`, `tags` for a `string` holding a comma-separated set'
                step:
                    type: number
                    description: "How coarsely a `control` moves, in the field's own unit. Ignored without one, and defaults to 1"
                min:
                    type: number
                    description: '`number` only: the smallest value that will be accepted, inclusive'
                max:
                    type: number
                    description: '`number` only: the largest value that will be accepted, inclusive'
                placeholder:
                    type: string
                    maxLength: 400
                help:
                    type: string
                    maxLength: 2000
                options:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ConfigFieldOption'
                optionsFrom:
                    '$ref': '#/components/schemas/ConfigFieldOptionSource'
                    description: "Choices only the console can enumerate. Merged where a plugin's own suggestions are, and outranked by them"
                columns:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ConfigFieldColumn'
                    description: '`list` only, and ignored elsewhere'
                dependsOn:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Key of the field this one is only relevant to'
                rangeWith:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Key of the `number` field that is the upper end of the range this one opens, declared on the lower end only. Still two settings, each validated by name; the console draws them as one control whose handles cannot cross'
            required: [key, label, type]
            description: "Mirrors the plugin SDK's `ConfigField`: enough for a console to render the settings form with no per-plugin code"
        PluginSummary:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 200
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                version:
                    type: string
                    minLength: 1
                    maxLength: 100
                capabilities:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 100
                status:
                    '$ref': '#/components/schemas/PluginStatus'
                enabled:
                    type: boolean
                description:
                    type: string
                    maxLength: 2000
                icon:
                    type: string
                    maxLength: 2000
                configFields:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ConfigFieldDescriptor'
                secretsConfigured:
                    type: object
                    additionalProperties:
                        type: boolean
                    description: 'Whether a value is currently stored, per `secret` field under its own key and per `secret` cell under `field/rowId/column`. Never the value itself'
                firstEnabledAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'When this plugin was first ever enabled. Absent means it never has been, so the console asks before it is'
            required:
                - id
                - name
                - version
                - capabilities
                - status
                - enabled
                - configFields
                - secretsConfigured
            description: 'A plugin as the settings list sees it. Carries no configured VALUES, only which secrets are set'
        PluginLogLevel:
            type: string
            enum: [debug, info, warn, error]
        PluginLogEntry:
            type: object
            properties:
                ts:
                    type: string
                    maxLength: 40
                level:
                    '$ref': '#/components/schemas/PluginLogLevel'
                text:
                    type: string
                    maxLength: 65536
                    description: 'Must match MAX_LINE_BYTES_CEILING in apps/api/src/logging/rotating.log.store.ts. Change both together'
            required: [ts, level, text]
        PluginLogPage:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                level:
                    '$ref': '#/components/schemas/PluginLogLevel'
                entries:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PluginLogEntry'
                    description: 'Newest first, as the activity feed and the script history send. The download is the file as written, oldest first'
            required: [pluginId, level, entries]
        PluginLogQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 2000
                level:
                    '$ref': '#/components/schemas/PluginLogLevel'
        PluginLogLevelInput:
            type: object
            properties:
                level:
                    '$ref': '#/components/schemas/PluginLogLevel'
            required: [level]
        PluginDetail:
            allOf:
                - '$ref': '#/components/schemas/PluginSummary'
                - type: object
                  properties:
                      config:
                          type: object
                          additionalProperties: {}
                      lastError:
                          type: string
                          maxLength: 4000
                      oauthConnected:
                          type: boolean
                      logLevel:
                          '$ref': '#/components/schemas/PluginLogLevel'
                  required: [config, logLevel]
        PluginConfigInput:
            type: object
            properties:
                config:
                    type: object
                    additionalProperties: {}
            required: [config]
            description: 'A submitted settings form. Secret values arrive in here and are never echoed back'
        GrantDecision:
            type: string
            enum: [allowed, denied]
            description: "What a plugin may do with a capability it asked for. Denied is the default and needs no row: a\ncapability is refused until somebody allows it, so \"never answered\" and \"refused\" are one state"
        PluginGrant:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                pluginName:
                    type: string
                    minLength: 1
                    maxLength: 200
                capability:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: "The host's own id for it, e.g. `network.open`"
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What the host calls the capability'
                describes:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: "What allowing it opens up, in the station's words"
                reason:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: "Why this plugin says it needs it, in the plugin's words"
                decision:
                    '$ref': '#/components/schemas/GrantDecision'
            required: [pluginId, pluginName, capability, label, describes, reason, decision]
            description: "One capability a plugin asked for, with the station's answer. The ask is the plugin's manifest and\nthe answer is a row, so a plugin that stops asking stops appearing here whatever was stored"
        PluginGrantList:
            type: object
            properties:
                grants:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PluginGrant'
                    description: 'Every capability every installed plugin is asking for, refused ones included'
            required: [grants]
        PluginGrantInput:
            type: object
            properties:
                capability:
                    type: string
                    minLength: 1
                    maxLength: 100
                decision:
                    '$ref': '#/components/schemas/GrantDecision'
            required: [capability, decision]
        PluginTestResult:
            type: object
            properties:
                ok:
                    type: boolean
                message:
                    type: string
                    maxLength: 4000
            required: [ok]
            description: "Outcome of the plugin's own `testConnection()`"
        PluginFieldSuggestions:
            type: object
            properties:
                fields:
                    type: object
                    additionalProperties:
                        type: array
                        items:
                            '$ref': '#/components/schemas/ConfigFieldOption'
                    description: 'Keys the plugin had nothing to say about are simply absent, rather than present and empty'
                supported:
                    type: boolean
                    description: "False when the plugin does not implement suggestions at all, so a console can tell \"nothing to\nsuggest\" from \"asked and got nothing\", and draw a refresh control only where one would do something"
            required: [fields, supported]
            description: "Live choices for a plugin's config fields, keyed by field key, out of the plugin's own\n`suggestConfigOptions()`. What `ConfigFieldDescriptor.options` cannot be: fixed when the manifest\nwas written, where these are whatever the operator's own server currently says"
        PluginOAuthStart:
            type: object
            properties:
                url:
                    type: string
                    format: uri
            required: [url]
            description: "Where the console should send the browser to obtain the operator's consent. Reported rather than\nredirected to: the route is behind the Bearer floor, so a browser cannot follow a redirect from it"
        PluginOAuthResult:
            type: object
            properties:
                pluginId:
                    type: string
                    minLength: 1
                    maxLength: 200
                ok:
                    type: boolean
                message:
                    type: string
                    maxLength: 4000
            required: [pluginId, ok]
            description: 'Outcome of an OAuth callback'
        PluginOAuthCallbackQuery:
            type: object
            properties:
                code:
                    type: string
                    maxLength: 2048
                state:
                    type: string
                    maxLength: 400
                error:
                    type: string
                    maxLength: 400
                ubi:
                    type: string
                    maxLength: 400
                token:
                    type: string
                    maxLength: 2048
                    description: "What a desktop-style flow returns instead of `code`: the provider mints a token before the\nconsent screen and hands the same one back, which the plugin exchanges for a session. Last.fm's\nauth works this way. Listed here because the route parses this query strictly, so an\nundeclared parameter is a 400 before any plugin code runs"
        ProductionCastMember:
            type: object
            properties:
                role:
                    type: string
                    enum: [host, caller]
                name:
                    type: string
                    maxLength: 200
                    description: 'What they are called on air'
                persona:
                    type: string
                    maxLength: 100
                    description: 'The persona key, for a link back to the character'
            required: [role]
            description: "One person in a production: the presenter, or somebody cast to phone in. A snapshot rather than a\nreference, because the persona it names may be edited or deleted while the programme is still being\nmade and what the turns were written as has to be what an operator reads back"
        Production:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'What sort of production: podcast, bulletin, feature. Free text, so a station that wants a documentary strand needs no migration'
                title:
                    type: string
                    minLength: 1
                    maxLength: 300
                brief:
                    type: string
                    maxLength: 4000
                    description: "What was asked for, in the operator's own words. Distinct from the title, which is only a label"
                personaId:
                    type: string
                    maxLength: 100
                    description: "Who presents it. Absent falls back to the station's active persona when a pass runs"
                writingMode:
                    type: string
                    enum: [quick, outlined, polished]
                    description: 'How many passes to spend on it'
                targetMs:
                    type: integer
                    minimum: 1000
                    description: 'How long it should run. What the beat count and the per-beat word budgets are computed from'
                state:
                    type: string
                    enum:
                        - planned
                        - outlining
                        - drafting
                        - checking
                        - rendering
                        - stitching
                        - ready
                        - aired
                        - failed
                        - cancelled
                    readOnly: true
                    description: '`stitching` is the beats being joined into one piece of audio, and it leads to `ready` whether that worked or not'
                error:
                    type: string
                    maxLength: 2000
                    readOnly: true
                    description: 'Why making it did not work'
                scheduledFor:
                    type: string
                    format: date-time
                    description: 'When it should air. Absent means as soon as it is made'
                cancelledAt:
                    type: string
                    format: date-time
                    readOnly: true
                beats:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'How many beats exist so far, which is how far along the drafting is'
                cast:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ProductionCastMember'
                    readOnly: true
                    description: 'Who is on it, decided by the first pass that ran. Empty for one nobody has started, and for a programme the presenter reads alone'
                createdAt:
                    type: string
                    format: date-time
                    readOnly: true
            required: [id, kind, title, writingMode, targetMs, state, beats, cast, createdAt]
            description: 'Something the station makes rather than something it says: several beats of speech, written in several passes, that airs as one block'
        ProductionList:
            type: object
            properties:
                productions:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Production'
            required: [productions]
        ProductionRequest:
            type: object
            properties:
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                title:
                    type: string
                    minLength: 1
                    maxLength: 300
                    description: 'Absent is named after its kind and the moment it was asked for, which is what somebody taking a call now wants rather than a box to fill in'
                brief:
                    type: string
                    maxLength: 4000
                personaId:
                    type: string
                    maxLength: 100
                writingMode:
                    type: string
                    enum: [quick, outlined, polished]
                    description: "Absent takes the station's `render.productionWritingMode`"
                targetMs:
                    type: integer
                    minimum: 1000
                scheduledFor:
                    type: string
                    format: date-time
            description: 'What an operator asks for. Everything else about a production is decided by the passes that make it'
        Segment:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                kind:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'What sort of element it is: `ident`, `stinger`, `talkbreak`, `news`'
                state:
                    type: string
                    enum: [planned, writing, written, rendering, ready, failed]
                    description: 'One state per stage of making it. Only `ready` can go on air; the station skips anything else rather than waiting for it'
                label:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'What the console calls it, and what the mount is labelled with while it airs'
                source:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'Who made it: `library` for a file dropped into the inbox'
                playable:
                    type: boolean
                    description: 'Whether there is audio behind it yet'
                script:
                    type: string
                    maxLength: 20000
                    description: 'The words, for anything that speaks. Absent for an imported recording'
                spokenScript:
                    type: string
                    maxLength: 20000
                    description: "The words as the speech engine was handed them: symbols said, years read as a person reads them, the station's pronunciation list applied. Absent until something has spoken it"
                sourcePath:
                    type: string
                    maxLength: 1000
                    description: 'The file in the inbox this came from. The bytes were copied, so emptying the inbox does not take it off the air'
                durationMs:
                    type: integer
                    minimum: 0
                    description: 'How long it runs. A display value: the player measures the audio itself'
                error:
                    type: string
                    maxLength: 2000
                    description: 'Why it is `failed`'
                voice:
                    type: string
                    maxLength: 100
                    description: "The station's own name for the voice this is said in, e.g. `host`. Absent means the speech plugin's default"
            required: [id, kind, state, label, source, playable]
            description: 'One thing the station can play that is not a record'
        SegmentCreate:
            type: object
            properties:
                label:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'What the console calls it, and what the mount is labelled with while it airs'
                script:
                    type: string
                    minLength: 1
                    maxLength: 20000
                    description: 'The words to say'
                kind:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'What sort of element it is. Defaults to `talkbreak`'
                voice:
                    type: string
                    maxLength: 100
                    description: 'A station voice name the speech plugin knows how to map. Absent uses its default'
            required: [label, script]
            description: 'Something for the station to say, before anything has said it'
        SegmentUpload:
            type: object
            properties:
                file:
                    type: string
                    format: binary
                    description: 'The audio itself. mp3, wav, ogg, flac or m4a, and at most 50 MB'
                kind:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'What sort of element it is, which is also the directory it is filed under. A kind nothing else uses becomes a bookable band on the format clock'
                label:
                    type: string
                    minLength: 1
                    maxLength: 400
                    description: 'What the console calls it, and what the mount is labelled with while it airs. Derived from the filename when absent'
            required: [file, kind]
            description: "A recording arriving from the browser, as multipart form parts.\n\nDocumentation rather than validation: a multipart body reaches the service as the raw parser and\nthe generated client types the body as `FormData`, so nothing checks this shape. It says what to\nsend"
        SegmentList:
            type: object
            properties:
                segments:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Segment'
            required: [segments]
            description: 'Everything the station can play that is not a record'
        Voice:
            type: object
            properties:
                id:
                    type: string
                    maxLength: 100
                    description: "What to pass as a segment's `voice`. Empty means the plugin's own default"
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What the console calls it'
                description:
                    type: string
                    maxLength: 500
                    description: 'What it sounds like, or what it maps to on the engine'
            required: [id, label]
            description: 'A voice the station can be asked to speak in'
        VoiceList:
            type: object
            properties:
                voices:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Voice'
                pluginId:
                    type: string
                    maxLength: 200
                    description: 'Which plugin answered. Absent when nothing can speak'
                reason:
                    type: string
                    maxLength: 500
                    description: 'Why there are no voices, when there are none'
            required: [voices]
            description: "The voices the station's current speech plugin offers"
        ScriptOutcome:
            type: string
            enum: [written, declined, failed]
            description: 'Whether there are words, and if not, which way it went wrong'
        ScriptNeighbour:
            type: object
            properties:
                title:
                    type: string
                    minLength: 1
                    maxLength: 500
                artist:
                    type: string
                    minLength: 1
                    maxLength: 500
                facts:
                    type: array
                    items:
                        type: string
                        maxLength: 1000
                    description: 'What it was shown about the record. A break that said nothing interesting and one that was TOLD nothing interesting read the same from the script alone'
            required: [title, artist]
            description: 'A record a writer was told about, kept as it was told'
        ScriptUsage:
            type: object
            properties:
                inputTokens:
                    type: integer
                    minimum: 0
                outputTokens:
                    type: integer
                    minimum: 0
                totalTokens:
                    type: integer
                    minimum: 0
            description: 'What the provider said the attempt cost, when it said anything'
        ScriptPromptMessage:
            type: object
            properties:
                role:
                    type: string
                    minLength: 1
                    maxLength: 50
                content:
                    type: string
                    maxLength: 100000
            required: [role, content]
            description: 'One turn of the conversation a writer sent'
        ScriptAttempt:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                at:
                    type: string
                    format: date-time
                kind:
                    type: string
                    minLength: 1
                    maxLength: 50
                    description: 'What sort of break it was for: `talkbreak`, `welcome`, `news`'
                writer:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'The binding that produced or declined it'
                outcome:
                    '$ref': '#/components/schemas/ScriptOutcome'
                personaKey:
                    type: string
                    maxLength: 100
                    description: "Who was presenting, as the persona's own key. Absent means nobody was, which is an ordinary state. Stamped on every attempt including the declined ones, so a character whose model breaks are all being refused is visible rather than hidden behind the floor"
                label:
                    type: string
                    maxLength: 400
                script:
                    type: string
                    maxLength: 20000
                    description: 'The words. Absent for an attempt that produced none'
                model:
                    type: string
                    maxLength: 200
                    description: 'The model that said it, for a writer that used one'
                source:
                    type: string
                    maxLength: 200
                    description: 'What the line was rendered from, for a writer working from something an operator can edit'
                reason:
                    type: string
                    maxLength: 2000
                    description: 'Why, for anything that is not `written`'
                segmentId:
                    type: string
                    maxLength: 100
                    description: 'The segment this was for, while it is still known. The row outlives it'
                previous:
                    '$ref': '#/components/schemas/ScriptNeighbour'
                next:
                    '$ref': '#/components/schemas/ScriptNeighbour'
                durationMs:
                    type: integer
                    minimum: 0
                    description: 'How long the attempt took'
                usage:
                    '$ref': '#/components/schemas/ScriptUsage'
                raw:
                    type: string
                    maxLength: 100000
                    description: 'The answer before anything read it. Only while `llm.captureWrites` is on'
                prompt:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScriptPromptMessage'
                    description: 'What the writer sent. Only while `llm.captureWrites` is on'
                rating:
                    '$ref': '#/components/schemas/ScriptRating'
                    readOnly: true
                    description: 'What the operator thought of it. ABSENT means nobody has said, which `neutral` does not'
            required: [id, at, kind, writer, outcome]
            description: 'One attempt to write something the station would say, including the ones that came to nothing'
        ScriptRating:
            type: string
            enum: [liked, neutral, disliked]
            description: "What an operator thought of something the station said.\n\nThe catalog's three spellings exactly, and deliberately not a second vocabulary: an opinion is an\nopinion whether it is about a record or about a sentence, and `catalog/rating.ts` is the one place\nthe words and the column's numbers meet.\n\n`neutral` is a real answer rather than an absence. Rating something back to nothing is a thing an\noperator does, and it has to be distinguishable from never having listened, which is the field\nbeing absent on the attempt."
        ScriptRatingInput:
            type: object
            properties:
                rating:
                    '$ref': '#/components/schemas/ScriptRating'
            required: [rating]
        ScriptHistoryQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 200
                before:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Where the previous page ended. Opaque, and a keyset rather than an offset because rows arrive at the head continuously. Pass back whatever `nextBefore` said and nothing else'
                kind:
                    type: string
                    minLength: 1
                    maxLength: 50
                writer:
                    type: string
                    minLength: 1
                    maxLength: 100
                outcome:
                    '$ref': '#/components/schemas/ScriptOutcome'
                personaKey:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Everything ONE character has said. Absent is every character and none'
                segmentId:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Every attempt made for ONE break, which is how a console reaches the words behind an item of the running order. Absent is the whole history'
            description: 'One page of what the station has written, newest first'
        ScriptHistoryPage:
            type: object
            properties:
                attempts:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScriptAttempt'
                nextBefore:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The cursor for the page after this one, absent once the history has been read to its end'
            required: [attempts]
        SpeechPreviewRequest:
            type: object
            properties:
                text:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: "What to say. Far under a segment's 20000 because this is one break heard once, and the cap is what bounds a cache keyed on the words themselves"
                voice:
                    type: string
                    maxLength: 100
                    description: "A station voice name, as a segment's `voice`. Absent uses the plugin's own default"
            required: [text]
            description: 'Words to hear before anything has aired them'
        ScriptHistorySummaryQuery:
            type: object
            properties:
                hours:
                    type: integer
                    minimum: 1
                    maximum: 168
                    description: 'How far back to count. Defaults to 24, and a week at most, because past that the nightly sweep may already have taken the rows and the count would quietly be of what survived rather than of what happened'
            description: 'The window the counts cover'
        ScriptHistorySummaryRow:
            type: object
            properties:
                personaKey:
                    type: string
                    maxLength: 100
                    description: 'Absent means nobody was presenting, which is an ordinary state rather than a gap in the data'
                written:
                    type: integer
                    minimum: 0
                declined:
                    type: integer
                    minimum: 0
                    description: 'A decline is the writer registry working: the model had nothing to say and the floor covered for it'
                failed:
                    type: integer
                    minimum: 0
            required: [written, declined, failed]
            description: "One presenter's attempts in the window"
        ScriptHistorySummary:
            type: object
            properties:
                hours:
                    type: integer
                    minimum: 1
                    maximum: 168
                    description: 'The window actually counted, echoed so a console can label the numbers it draws'
                rows:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScriptHistorySummaryRow'
            required: [hours, rows]
            description: 'What each presenter has written lately, and over how long'
        SegmentScanResult:
            type: object
            properties:
                scanned:
                    type: integer
                    minimum: 0
                    description: 'Audio files seen, whether or not they were already known'
                imported:
                    type: integer
                    minimum: 0
                    description: 'Segments the station did not have before this pass'
                skipped:
                    type: integer
                    minimum: 0
                    description: 'Files passed over: not audio it can serve, or unreadable'
            required: [scanned, imported, skipped]
            description: 'What one pass over the inbox did'
        Pronunciation:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                written:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What appears in a script. Matched case-insensitively, and whole words only'
                spoken:
                    type: string
                    maxLength: 400
                    description: 'What the engine is handed instead, untouched. EMPTY is meaningful: it drops the words, which is the honest reading for a marker that got into a title and is not a word'
                state:
                    type: string
                    enum: [active, suggested, rejected]
                    description: '`active` is said. `suggested` is proposed and says nothing yet. `rejected` outlives the pass that proposed it, or the same article proposes it again forever'
                origin:
                    type: string
                    enum: [operator, gloss]
                    description: 'Who says so. `gloss` is a pronunciation key an encyclopaedia article printed for itself'
                sourceUrl:
                    type: string
                    maxLength: 2000
                    description: 'The article. Present on anything an operator did not type'
                sourceQuote:
                    type: string
                    maxLength: 2000
                    description: 'The sentence that says so, as it stands in the article, which is what the decision is actually made on'
                subjectKind:
                    type: string
                    enum: [track, album, artist]
                    description: 'What the article was about'
                subjectId:
                    type: string
                    maxLength: 100
                createdAt:
                    type: string
                    minLength: 1
                    maxLength: 40
            required: [id, written, spoken, state, origin, createdAt]
            description: 'One name the station says differently from how it is written'
        PronunciationList:
            type: object
            properties:
                pronunciations:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Pronunciation'
            required: [pronunciations]
            description: "The station's lexicon, oldest first"
        PronunciationWrite:
            type: object
            properties:
                written:
                    type: string
                    minLength: 1
                    maxLength: 200
                spoken:
                    type: string
                    maxLength: 400
                    description: 'Empty drops the words rather than saying them'
            required: [written, spoken]
            description: 'A name and how to say it'
        PronunciationStateWrite:
            type: object
            properties:
                state:
                    type: string
                    enum: [active, suggested, rejected]
            required: [state]
            description: 'Accepting a proposal, turning one down, or taking an entry out of use without losing it'
        PronunciationQuery:
            type: object
            properties:
                state:
                    type: string
                    enum: [active, suggested, rejected]
                    description: 'Absent is all of it'
            description: 'Which part of the lexicon to read'
        Pad:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                board:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'Which directory it arrived in. Provenance: what reaches it is a set'
                sets:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    readOnly: true
                    description: 'The keys of the sets it is on. Empty means it is in the library and nothing can hit it'
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What a script writes: `[sfx:airhorn]`'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                durationMs:
                    type: integer
                    minimum: 0
                loudnessLufs:
                    type: number
                    description: 'How loud it came out, once something measured it. Absent on a station with no analyzer, which is ordinary'
                source:
                    type: string
                    minLength: 1
                    maxLength: 50
                    readOnly: true
                    description: 'Who put the file there: `library` for one the operator dropped in, `upload` or `url` for one the console wrote. It decides whether the console may delete it'
                sourcePath:
                    type: string
                    maxLength: 500
                    description: 'The file in the library directory it was imported from, so the console can say where it came from'
                lastUsedAt:
                    type: string
                    format: date-time
                    description: 'When it was last hit. Absent for one nothing has reached for yet'
                state:
                    type: string
                    enum: [active, rejected]
            required: [id, board, sets, name, label, source, state]
            description: "One sound on a soundboard, as the console draws it.\n\n`name` is what a script writes to hit it and `label` is what a person reads: two columns rather\nthan one, because a token for a model and prose for an operator are different things and the\nfilename produces both"
        PadUpload:
            type: object
            properties:
                file:
                    type: string
                    format: binary
                    description: 'The audio itself. mp3, wav, ogg, flac or m4a, and at most 25 MB'
                board:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The directory it is filed under, which is also the set it joins. A new name makes both'
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What a script will write. Derived from the filename when absent, and the FILE is named after this either way'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What the console calls it. Derived from the filename when absent'
            required: [file, board]
            description: "A sound arriving from the browser, as multipart form parts.\n\nDocumentation rather than validation: a multipart body reaches the service as the raw parser and\nthe generated client types the body as `FormData`, so nothing checks this shape. It says what to\nsend"
        PadFetch:
            type: object
            properties:
                url:
                    type: string
                    format: uri
                    description: 'Where the audio is. Followed once, bounded, and refused unless what comes back is a format the station serves'
                board:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The directory it is filed under, which is also the set it joins'
                name:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What a script will write. Derived from the address when absent'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
            required: [url, board]
            description: "A sound the station is being told to go and get.\n\nThe operator names the address, so this is them choosing a file exactly as dropping one in the\nlibrary is. Nothing inspects what comes back and nothing records a claim about its licence -- see\n`docs/internals/render.md` under \"Pads\", whose line is redistribution rather than use"
        PadList:
            type: object
            properties:
                pads:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Pad'
                sets:
                    type: array
                    items:
                        '$ref': '#/components/schemas/PadSet'
            required: [pads, sets]
            description: 'Every sound the station holds, and the sets over it'
        PadSet:
            type: object
            properties:
                id:
                    type: string
                    format: uuid
                    readOnly: true
                key:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The slug a persona names. A directory in the pad library makes one of these'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                position:
                    type: integer
                    minimum: 0
                pads:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'How many sounds are on it. Zero is ordinary: it is what a set looks like before anybody drops a file'
                personas:
                    type: array
                    items:
                        type: string
                        minLength: 1
                        maxLength: 200
                    readOnly: true
                    description: 'Who is pointed at it, so a rename or a delete can say what it is about to unpoint'
            required: [id, key, label, position, pads, personas]
            description: "A named collection of pads: what a presenter is actually handed.\n\nOne library, cut as many ways as an operator likes. `personas.soundboard` holds the `key`, so\nrenaming a set unpoints every persona naming it — which is why `personas` says who those are"
        PadSetWrite:
            type: object
            properties:
                key:
                    type: string
                    minLength: 1
                    maxLength: 200
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                position:
                    type: integer
                    minimum: 0
            required: [key, label]
            description: 'A set an operator is naming, or renaming'
        PadSetMembership:
            type: object
            properties:
                padId:
                    type: string
                    format: uuid
                'on':
                    type: boolean
            required: [padId, 'on']
            description: 'Which pad, and whether it is on the set'
        PadState:
            type: object
            properties:
                state:
                    type: string
                    enum: [active, rejected]
            required: [state]
            description: 'Turning a pad down, or putting one back'
        PadScanResult:
            type: object
            properties:
                scanned:
                    type: integer
                    minimum: 0
                    description: 'Audio files seen, whether or not anything changed'
                imported:
                    type: integer
                    minimum: 0
                    description: 'Sounds the station did not have before'
                replaced:
                    type: integer
                    minimum: 0
                    description: 'Slots whose file changed under them, which every script naming them now plays'
                contested:
                    type: integer
                    minimum: 0
                    description: 'Sounds that reached the library but not their set, because it already answered to their name. In the library and unreachable until somebody says where they go'
                skipped:
                    type: integer
                    minimum: 0
                    description: 'Files passed over: not audio, unreadable, or named something no script could write'
            required: [scanned, imported, replaced, contested, skipped]
            description: 'What one pass over the pad library did'
        ScheduleSlot:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                label:
                    type: string
                    maxLength: 200
                    description: "What the operator calls this stretch of the day. Becomes the broadcast's name"
                startsAtMinutes:
                    type: integer
                    minimum: 0
                    maximum: 1439
                    description: "When it starts, as minutes past midnight on the station's clock"
                endsAtMinutes:
                    type: integer
                    minimum: 0
                    maximum: 1439
                    description: 'When it stops, in the same terms. Before the start means the block runs past midnight, which is ordinary for a late show; equal to it means a full twenty-four hours'
                days:
                    type: array
                    items:
                        type: integer
                        minimum: 0
                        maximum: 6
                    description: 'The weekdays it runs on, Sunday 0. Absent or empty means every day'
                sourcePluginId:
                    type: string
                    maxLength: 200
                    description: 'The plugin the records come from. Absent, with no playlist, is a slot the station fills itself'
                sourcePlaylistId:
                    type: string
                    maxLength: 500
                sourceChartId:
                    type: string
                    maxLength: 400
                    description: 'A published chart to play instead, as `pluginId:chartId`. An ALTERNATIVE to the playlist pair rather than a companion, and it wins if both are sent: a playlist names copies the station can already fetch and a chart names records it has to look up'
                sourceChartOrder:
                    type: string
                    enum: [countdown, ranked, unordered]
                    description: 'Which way round that chart is played. Absent is `countdown`, which ends on number one. Ignored without `sourceChartId`'
                personaId:
                    type: string
                    maxLength: 100
                    description: "Who hosts this stretch of the day. Absent means the station's own active persona"
                brief:
                    type: string
                    maxLength: 500
                    description: "What this stretch of the day is asked to play, in the operator's own words. The same ceiling `PutOnAirInput.brief` has, because a changeover builds one of those from this and the two boxes are one field set on the console"
                eraFrom:
                    type: integer
                    minimum: 1900
                    maximum: 2100
                    description: 'The earliest release year this stretch of the day plays. Absent means no lower bound, and a record whose year the catalog does not know is played whatever the period'
                eraTo:
                    type: integer
                    minimum: 1900
                    maximum: 2100
                    description: 'The latest release year, on the same terms. Set with `eraFrom` for a decade; either may stand alone'
                callins:
                    type: boolean
                    description: "Whether somebody phones in during this stretch of the day. Absent leaves the station's own setting standing, exactly as it does when an operator briefs a broadcast by hand; a `setlist` or a `feature` takes no calls whatever this says"
                mode:
                    type: string
                    enum: [rotation, setlist, feature]
                onEnd:
                    type: string
                    enum: [extend, repeat, stop]
            required: [id, label, startsAtMinutes, endsAtMinutes, mode, onEnd]
            description: "One stretch of the station's day: from this time, on these days, the station plays this"
        ScheduleSlotList:
            type: object
            properties:
                slots:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScheduleSlot'
            required: [slots]
        ScheduleTimetableQuery:
            type: object
            properties:
                from:
                    type: string
                    minLength: 10
                    maxLength: 10
                    description: "The first day to draw, as `YYYY-MM-DD` on the station's own calendar. Absent means the station's today, which is the only way a caller that does not know the station's timezone can anchor"
                days:
                    type: integer
                    minimum: 1
                    maximum: 31
                    description: 'How many days from `from`. Defaults to a week'
            description: "A window of the station's day to draw"
        ScheduleTimetable:
            type: object
            properties:
                from:
                    type: string
                    minLength: 10
                    maxLength: 10
                    description: "The range actually drawn, echoed so a caller steps forward and back by adding days to a string rather than by knowing the station's timezone"
                days:
                    type: integer
                    minimum: 1
                    maximum: 31
                occurrences:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScheduleOccurrence'
            required: [from, days, occurrences]
            description: "The station's day as blocks, ready to draw"
        ScheduleOccurrence:
            type: object
            properties:
                slotId:
                    type: string
                    minLength: 1
                    maxLength: 100
                label:
                    type: string
                    maxLength: 200
                start:
                    type: string
                    minLength: 19
                    maxLength: 19
                    description: "`YYYY-MM-DD HH:mm:ss` on the station's own clock, deliberately carrying no timezone offset: it is a reading rather than a moment, so it draws as written wherever the console is running"
                end:
                    type: string
                    minLength: 19
                    maxLength: 19
                    description: 'The same, exclusive. Every block stays inside one day, so a slot running past midnight arrives as two'
            required: [slotId, label, start, end]
            description: 'One block: this slot, on this day, between these two times'
        ScheduleNow:
            type: object
            properties:
                now:
                    type: string
                    minLength: 19
                    maxLength: 19
                    description: "What time it is on the station's own clock, in the same zone-naive `YYYY-MM-DD HH:mm:ss` shape as a block's ends. It is here so a caller can say how much of the block is left without knowing the station's timezone: subtracting two readings taken in one frame is arithmetic, deriving one is not"
                slotId:
                    type: string
                    maxLength: 100
                    description: 'The slot in force at this instant. Absent means the station has no schedule'
                airingSlotId:
                    type: string
                    maxLength: 100
                    description: "The slot the running order actually belongs to. Different from the one above while an operator's own choice holds, which it does until the next slot begins"
                upcoming:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ScheduleOccurrence'
                    description: 'The block on now, if there is one, and the few that follow it, earliest first. Empty for a station with nothing scheduled from here on. A gap is simply absent, exactly as it is on the timetable: what plays there is the sustaining source rather than a block'
            required: [now, upcoming]
            description: 'Which slot the clock says should be on right now, and what follows it'
        SettingGroup:
            type: string
            enum:
                - station
                - stream
                - housekeeping
                - mail
                - rotation
                - playout
                - render
                - llm
                - analysis
                - schedule
                - personas
            description: 'Which part of the console owns a setting. Every one of these but `schedule` and `personas` is a section of the settings page; `schedule` is edited on the schedule page, beside the timetable it describes, and `personas` on the characters page, beside the names it stands behind.'
        StationSettingDescriptor:
            allOf:
                - '$ref': '#/components/schemas/ConfigFieldDescriptor'
                - type: object
                  properties:
                      group:
                          '$ref': '#/components/schemas/SettingGroup'
                  required: [group]
        StationSettings:
            type: object
            properties:
                descriptors:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StationSettingDescriptor'
                values:
                    type: object
                    additionalProperties: {}
                    description: 'Every NON-secret setting, with defaults filled in for whatever is not stored'
                configured:
                    type: object
                    additionalProperties:
                        type: boolean
                    description: 'One entry per `secret` setting: whether a value is currently stored. Never the value itself'
            required: [descriptors, values, configured]
            description: 'Every station setting, with what it is currently worth'
        StationSettingsInput:
            type: object
            properties:
                values:
                    type: object
                    additionalProperties: {}
            required: [values]
            description: "A submitted settings form. Partial: a key that is present is written, a key that is absent is left\nalone, so a console may send one field. A secret submitted blank clears it"
        Pagination:
            type: object
            properties:
                page:
                    type: integer
                    minimum: 0
                    default: 0
                    description: 'The page number'
                pageSize:
                    type: integer
                    minimum: 1
                    maximum: 100
                    default: 25
                    description: 'The page size'
                sort:
                    type: string
                    enum: [asc, desc]
                    default: desc
                    description: 'The sort order'
                total:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'The total number of items'
            required: [page, pageSize, sort, total]
            description: 'Represents a pagination object'
        LogLevel:
            type: string
            enum: [trace, debug, info, warn, error]
            description: "Five values, where `PluginLogLevel` next door has four. The plugin enum is the narrower one on\npurpose — that is the vocabulary a plugin's own `PluginLogger` offers — while `api.log` is written\nby `DeadairLogger`, which tees every level the app-wide `Logger` has, `trace` included. Narrowing\nhere would make a `trace` line unrepresentable in the type of the surface that reads the file it\nis in."
        LogSource:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 40
                    description: 'A closed set the API owns: `api`, `liquidsoap`, `shim`. Never a path'
                label:
                    type: string
                    minLength: 1
                    maxLength: 80
                description:
                    type: string
                    maxLength: 300
                    description: 'What writes it, in a sentence, because "shim" means nothing to somebody who has not read the tree'
                present:
                    type: boolean
                    description: 'Whether the file is there at all. A station that never ran the stream has no stream logs, which is a state rather than a fault'
                levels:
                    type: boolean
                    description: 'Whether its lines carry a level, so the console knows whether to offer the filter'
                bytes:
                    type: integer
                    minimum: 0
                    description: 'Retained size across every segment. Zero when absent'
                lastWriteAt:
                    type: string
                    format: date-time
                    description: 'Absent when nothing has ever been written'
            required: [id, label, description, present, levels, bytes]
            description: 'One log file this install has, whether or not anything has been written to it'
        LogSourceList:
            type: object
            properties:
                sources:
                    type: array
                    items:
                        '$ref': '#/components/schemas/LogSource'
                    description: 'Every source, in a fixed order, including the ones that are not present'
            required: [sources]
        LogLine:
            type: object
            properties:
                ts:
                    type: string
                    maxLength: 40
                    description: 'Absent on a line this API did not write, and on one of its own that did not parse'
                level:
                    '$ref': '#/components/schemas/LogLevel'
                    description: 'Absent for the same two reasons'
                text:
                    type: string
                    maxLength: 65536
                    description: 'Must match MAX_LINE_BYTES_CEILING in apps/api/src/logging/rotating.log.store.ts. Change both together'
            required: [text]
            description: 'One line, as far as it could be read back'
        LogPage:
            type: object
            properties:
                sourceId:
                    type: string
                    minLength: 1
                    maxLength: 40
                level:
                    '$ref': '#/components/schemas/LogLevel'
                    description: 'The minimum severity that was applied. Absent when the source carries no levels, so a filter that did nothing cannot look as though it worked'
                truncated:
                    type: boolean
                    description: "Whether the read hit its byte budget, so the oldest line here is not the file's first"
                lines:
                    type: array
                    items:
                        '$ref': '#/components/schemas/LogLine'
                    description: 'Newest first, as the plugin log page, the activity feed and the script history all send'
            required: [sourceId, truncated, lines]
        LogQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 2000
                level:
                    '$ref': '#/components/schemas/LogLevel'
                    description: 'Ignored by a source whose lines carry no level'
        AttentionEvidence:
            type: object
            properties:
                label:
                    type: string
                    minLength: 1
                    maxLength: 300
                    description: "The thing itself, as an operator would name it: a record's title and who made it"
                reason:
                    type: string
                    minLength: 1
                    maxLength: 600
                    description: "Why THIS one, in the station's own sentence. The row's `detail` says what the category means; this says what happened here"
                route:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The page holding the whole of it. Absent where there is no page for it, which the running order can hold: a record the catalog never ingested has none'
            required: [label, reason]
            description: "One concrete thing an attention row is about, so the reason does not live a page away.\n\nThe row above it counts and categorises; this names. \"4 records have no copy left that will play\"\nis a category an operator can do nothing with until they know WHICH four and WHY each one, and\nevery one of those facts was already stored — the fetch error on `track_audio.last_error`, the\nprovider's refusal on `track_sources.playable` — and reachable only by finding the record and\nhovering a cell on its page. This is that fact travelling with the row that counted it."
        AttentionItem:
            type: object
            properties:
                code:
                    type: string
                    minLength: 1
                    maxLength: 60
                    description: 'What this is, as a stable key: `silence`, `benchedCopies`, `noPersona`. The console groups and counts on it rather than on the sentence'
                severity:
                    type: string
                    enum: [failure, warning, notice]
                    description: '`failure` is the station not doing its job, `warning` is something failing beside a station that is working, and `notice` is a thing nobody has set up yet. A notice is not a fault and must not be drawn as one'
                title:
                    type: string
                    minLength: 1
                    maxLength: 120
                    description: 'The line an operator reads first'
                detail:
                    type: string
                    minLength: 1
                    maxLength: 800
                    description: 'The whole of it, in a sentence. Where the station already has words for a fact, these are those words rather than a second phrasing of them'
                route:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The console page that can do something about it'
                count:
                    type: integer
                    minimum: 0
                    description: 'How many things this is about, where that is a number rather than a state'
                evidence:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AttentionEvidence'
                    description: 'A HANDFUL of the things this row is about, never all of them: this answer is polled and a row about four hundred records must not be four hundred sentences. `count` stays the true figure, and a console showing fewer than it says so'
            required: [code, severity, title, detail, route]
            description: "One thing that wants the operator's attention, or the fact that nothing does"
        StationAttention:
            type: object
            properties:
                items:
                    type: array
                    items:
                        '$ref': '#/components/schemas/AttentionItem'
            required: [items]
            description: 'Everything wrong or waiting, worst first'
        StationHeartbeat:
            type: object
            properties:
                name:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                startedAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'When the loop registered, which is when it was last (re)started'
                lastBeat:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'When it last completed a pass. Absent until it completes its first'
            required: [name, startedAt]
            description: "One loop the station runs, and when it last came round.\n\nTwo timestamps and no verdict, because the loop cannot supply one: a five-second reconcile and a\nnightly sweep are both healthy and no single threshold describes both. `Heartbeat` itself takes\nthis position — it answers how long it has been and lets the reader decide — and a `stalled`\nboolean here would be this module inventing the threshold that file deliberately refuses to.\n\n`lastBeat` is absent until a loop finishes its first pass, which is why `startedAt` is there: from\nthe two of them a reader can tell a loop that has never completed anything from one that stopped."
        StationBacklog:
            type: object
            properties:
                total:
                    type: integer
                    minimum: 0
                    readOnly: true
                cached:
                    type: integer
                    minimum: 0
                    readOnly: true
                measured:
                    type: integer
                    minimum: 0
                    readOnly: true
            required: [total, cached, measured]
            description: "How much of the library the station has actually looked at.\n\nThe counts `/catalog/tracks` already answers with, lifted out of a page of rows: a check-up wants\nthe sentence \"13 of 581 measured\" without asking for thirteen tracks to get it."
        StationCheckup:
            type: object
            properties:
                readAt:
                    type: string
                    format: date-time
                    readOnly: true
                    description: 'When this reading was taken, so a stale page cannot pass itself off as now'
                revision:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                    description: "The commit this station was built from, as the image's `org.opencontainers.image.revision` label says it. Unlike the sections below, absent is not a failed reader: it means nothing stamped this build, which is what a development tree and a hand-built image both are"
                version:
                    type: string
                    minLength: 1
                    maxLength: 50
                    readOnly: true
                    description: "The release this station is, as the image's `org.opencontainers.image.version` label says it. Absent on the same terms as `revision` and for a second reason: only a tagged build carries one, so a station following `latest` reports a commit and no version"
                heartbeats:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StationHeartbeat'
                    readOnly: true
                backlog:
                    '$ref': '#/components/schemas/StationBacklog'
                    readOnly: true
            required: [readAt]
            description: "One reading of the machinery, for a page that assembles the station's health.\n\nIt carries ONLY the two signals nothing else exposes. Everything else a check-up shows — the\nsilence verdict, the listener count, what needs somebody, the plugin statuses, the disk — is\nalready on a contract the console reads, and composing them again here would be a second answer\nthat can disagree with the first. `/playout/status` in particular is polled every two seconds for\nthe transport strip, so asking for it a second way would be a second reading of the same fact.\n\nEach section is OPTIONAL and absent means that reader failed. A page saying what is wrong is the\nworst place for one broken reader to take the whole answer down, which is the rule\n`StationAttentionService` already works to. `revision` is the one exception and says so on its\nown line: it cannot fail, so absent there means something else.\n\nThe revision is on THIS contract rather than composed from `/health`, which also reports it, and\nthat is not the second-answer problem the paragraph above describes. Both read one string from one\nplace at boot, so they cannot disagree. What they differ in is who can reach them: `/health` is\n`operation(internal)`, deliberately, so it generates no SDK method and the console cannot call it\n— which would leave \"which build is this\" answerable only from a shell, the one thing carrying it\nhere exists to fix."
        TraceOutcome:
            type: string
            enum: [ok, failed]
            description: "Whether a call produced what it was asked for. Two values on purpose: every finer distinction —\ntimed out, was preempted, came back empty — is a fact the caller knew and the recorder did not, so\nit lives in `detail` where it can be named"
        TraceSpan:
            type: object
            properties:
                at:
                    type: string
                    format: date-time
                    description: 'When the call ended, which is when its cost was known'
                op:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Dotted and stable: `job.run`, `plugin.invoke`, `llm.generate`'
                target:
                    type: string
                    maxLength: 300
                    description: 'Which one: a plugin and its method, or a model'
                ms:
                    type: integer
                    minimum: 0
                    description: 'How long it held, measured around the call rather than reported by it'
                outcome:
                    '$ref': '#/components/schemas/TraceOutcome'
                error:
                    type: string
                    maxLength: 300
                    description: 'The failure, summarized to a shape rather than a stack'
                detail:
                    type: object
                    additionalProperties: {}
                    description: 'Whatever this `op` is worth reading back: tokens, a finish reason, the bound it was given'
            required: [at, op, ms, outcome]
            description: 'One call inside a decision, and what it cost'
        TraceDecision:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: "The job id or the request id. Already the station's correlation id, never generated for this"
                kind:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'A queue name, or a method and path'
                parent:
                    type: string
                    maxLength: 200
                    description: 'The decision that enqueued this one. Absent on a request, a cron job and anything at boot'
                at:
                    type: string
                    format: date-time
                    description: 'When its first recorded call ended'
                ms:
                    type: integer
                    minimum: 0
                    description: 'Wall clock, off the `job.run` span. Zero for a decision recorded before that span existed'
                calls:
                    type: integer
                    minimum: 0
                    description: 'Everything it did, not counting the `job.run` that contains them'
                failed:
                    type: integer
                    minimum: 0
                    description: 'How many of those did not produce what they were asked for'
            required: [id, kind, at, ms, calls, failed]
            description: 'One decision, folded: a job execution or a request'
        TracesQuery:
            type: object
            properties:
                limit:
                    type: integer
                    minimum: 1
                    maximum: 200
                kind:
                    type: string
                    maxLength: 200
                    description: 'An exact queue name or route, for reading one kind of decision on its own'
                failedOnly:
                    type: boolean
                    description: 'Only decisions carrying at least one failed call'
            description: 'Which slice of the kept window to read'
        TracesPage:
            type: object
            properties:
                decisions:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TraceDecision'
                    description: 'Newest first'
                total:
                    type: integer
                    minimum: 0
                    description: 'How many the window holds before `limit`, so a page can say it is showing a slice'
                spans:
                    type: integer
                    minimum: 0
                    description: 'How many calls were read to answer, which is the honest cost of this page'
            required: [decisions, total, spans]
        TraceDetail:
            type: object
            properties:
                decision:
                    '$ref': '#/components/schemas/TraceDecision'
                spans:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TraceSpan'
                    description: 'In the order they happened'
                parent:
                    '$ref': '#/components/schemas/TraceDecision'
                    description: 'What enqueued this, when that decision is still inside the kept window'
                caused:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TraceDecision'
                    description: 'What this one went on to enqueue'
            required: [decision, spans, caused]
            description: 'One decision, its calls, and the decisions on either side of it'
        StorageStoreId:
            type: string
            enum: [tracks, art, segments, voices]
            description: 'Which store, as a stable id the console can key off rather than a name it renders.'
        StorageStore:
            type: object
            properties:
                id:
                    '$ref': '#/components/schemas/StorageStoreId'
                    readOnly: true
                label:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                    description: 'What to call it on a page'
                path:
                    type: string
                    minLength: 1
                    maxLength: 1000
                    readOnly: true
                    description: 'Where it is, so `du` and this can be compared'
                files:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Files actually there'
                bytes:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'What they weigh'
                rows:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Rows pointing at a file. Absent when no table backs this store'
                accountedBytes:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'What those rows say those files weigh. Absent where the table does not record a size'
                capBytes:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'The limit an operator set, where the store has one. Absent means no limit'
                orphanFiles:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Files no row claims. Reported and never cleaned up automatically'
                orphanBytes:
                    type: integer
                    minimum: 0
                    readOnly: true
                rowsWithNoFile:
                    type: integer
                    minimum: 0
                    readOnly: true
                    description: 'Claims whose file is not there. The station re-fetches or re-renders these'
            required: [id, label, path, files, bytes, orphanFiles, orphanBytes, rowsWithNoFile]
            description: "One content store: what is on disk, and what the database says should be.\n\nThe two halves are deliberately separate numbers rather than one reconciled figure. They disagree\nin two directions and each direction means something different — a file nothing claims is what a\ncrash between writing bytes and writing a row leaves behind, and a row whose file is gone is what\nan operator emptying a directory leaves. Reporting one number would hide both."
        StorageReport:
            type: object
            properties:
                readAt:
                    type: string
                    format: date-time
                    readOnly: true
                totalFiles:
                    type: integer
                    minimum: 0
                    readOnly: true
                totalBytes:
                    type: integer
                    minimum: 0
                    readOnly: true
                stores:
                    type: array
                    items:
                        '$ref': '#/components/schemas/StorageStore'
            required: [readAt, totalFiles, totalBytes, stores]
            description: "Every store, plus the number an operator actually wants first.\n\n`readAt` is not decoration: the figures come from walking directories, which is real I/O on a\nstation holding tens of thousands of files, so the answer is cached for a short while and this is\nwhat stops a page mistaking it for live."
        FetcherAuthorization:
            type: object
            properties:
                reachable:
                    type: boolean
                    description: 'Whether the fetcher answered at all. False makes every field below a default rather than a reading, so a fetcher that is merely down is never reported as one that was never authorized'
                configured:
                    type: boolean
                    description: 'Whether this install has a stream half yet. False means there is nothing here to authorize'
                authorized:
                    type: boolean
                    description: "Whether the fetcher holds its OWN stored authorization, which is the only kind Spotify's login accepts. False with a healthy plugin above it is a station that lists playlists perfectly and cannot fetch a single record"
                session:
                    type: boolean
                    description: 'Whether a login is established right now'
                loginError:
                    type: string
                    maxLength: 500
                    description: 'The last reason a login was refused. Present is not the same as fatal: a session may have recovered since'
                pendingUrl:
                    type: string
                    maxLength: 2000
                    description: 'An authorization already started and not yet finished, so an operator who lost the URL is given it back rather than having to start again'
                callbackUrl:
                    type: string
                    maxLength: 2000
                    description: 'The address the browser will be sent to and will not be able to load. Reported so the console can say which page is expected to fail, rather than leaving that looking like a fault. Absent when the fetcher did not answer, since it is the only thing that knows it'
            required: [reachable, configured, authorized, session]
            description: "What the station's track fetcher holds by way of a Spotify login"
        FetcherAuthorizationStart:
            type: object
            properties:
                authorizeUrl:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: 'The Spotify consent page, to be opened by the operator'
                expiresInMs:
                    type: integer
                    minimum: 0
                    description: 'How long this URL is good for. Starting another replaces it'
            required: [authorizeUrl, expiresInMs]
            description: 'An authorization to open in a browser'
        FetcherAuthorizationInput:
            type: object
            properties:
                redirectUrl:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: 'The address the browser ended up at, pasted whole. Taken apart by the fetcher rather than here, because two readings of one address is one of them being wrong eventually'
            required: [redirectUrl]
            description: 'The callback the browser could not deliver, handed over by the operator instead'
        FetcherAuthorizationFinished:
            type: object
            properties:
                username:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'The Spotify account that was authorized. Reported because an operator with two accounts in two browser profiles wants to know which one this station now is'
            required: [username]
            description: 'Which account the station now fetches as'
        Topic:
            type: object
            properties:
                id:
                    type: string
                    minLength: 1
                    maxLength: 100
                    readOnly: true
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'Which sort of break this is a subject for, as `segments.kind` spells it'
                key:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'A stable slug, unique within its kind. What the format clock points a band at and what a seeded topic is recognised by'
                label:
                    type: string
                    minLength: 1
                    maxLength: 200
                    description: 'What a break calls it out loud, so it is the phrasing you would want to hear'
                config:
                    type: object
                    additionalProperties: {}
                    description: "This sort of break's own settings for this subject, in the fields its kind declares. Nothing generic reads them"
                position:
                    type: integer
                    minimum: 0
                    description: 'Your own order, for the list. Two subjects never contest anything, so it means nothing else'
            required: [id, kind, key, label, config, position]
            description: "What a break can be about: a news category, and later a weather location. The operator's own vocabulary, per sort of break"
        TopicList:
            type: object
            properties:
                topics:
                    type: array
                    items:
                        '$ref': '#/components/schemas/Topic'
            required: [topics]
        TopicKindDescriptor:
            type: object
            properties:
                kind:
                    type: string
                    minLength: 1
                    maxLength: 100
                nounOne:
                    type: string
                    minLength: 1
                    maxLength: 100
                    description: 'What to call one of these: a news subject is a category and a weather subject is a location'
                nounMany:
                    type: string
                    minLength: 1
                    maxLength: 100
                description:
                    type: string
                    maxLength: 2000
                fields:
                    type: array
                    items:
                        '$ref': '#/components/schemas/ConfigFieldDescriptor'
            required: [kind, nounOne, nounMany, description, fields]
            description: "A sort of break that has subjects at all, and how one of its subjects is edited. `ConfigFieldDescriptor` is the plugins area's, shared for the reason a station setting shares it: one form component renders them all"
        TopicKindList:
            type: object
            properties:
                kinds:
                    type: array
                    items:
                        '$ref': '#/components/schemas/TopicKindDescriptor'
            required: [kinds]
        TopicQuery:
            type: object
            properties:
                kind:
                    type: string
                    maxLength: 100
                    description: 'One sort of break, or absent for every subject this station has named'
