// OpenAPI 3.0 specification for the Taboard API, served via Swagger UI at /api-docs. const STATUSES = ['open', 'queued', 'processing', 'done', 'failed', 'close']; const TYPES = ['backend', 'api', 'frontend', 'ui']; export const openapiSpec = { openapi: '3.0.3', info: { title: 'Taboard API', version: '1.0.0', description: 'Jira-style admin board + API backed by Supabase.' }, tags: [ { name: 'Health', description: 'Service health' }, { name: 'Projects', description: 'Project management' }, { name: 'Tickets', description: 'Ticket management (board)' }, { name: 'Flow', description: 'Lightweight ticket flow API for agents' }, { name: 'ChefSys', description: 'Chef system: menu items (foods/drinks/add-ons) & orders' } ], components: { schemas: { Error: { type: 'object', properties: { error: { type: 'string' } } }, Project: { type: 'object', properties: { id: { type: 'integer', readOnly: true }, name: { type: 'string' }, note: { type: 'string', nullable: true }, struc: { type: 'object', additionalProperties: true } } }, ProjectInput: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, note: { type: 'string', nullable: true }, struc: { oneOf: [{ type: 'object' }, { type: 'string' }], description: 'Object, or a JSON string (parsed server-side).' } } }, Ticket: { type: 'object', properties: { id: { type: 'integer', readOnly: true }, title: { type: 'string' }, detail: { type: 'string', nullable: true }, type: { type: 'string', enum: TYPES }, status: { type: 'string', enum: STATUSES }, message: { type: 'string', nullable: true }, logs: { type: 'string', nullable: true }, sort: { type: 'integer' }, subtic: { type: 'integer', nullable: true }, projectid: { type: 'integer', nullable: true }, project_name: { type: 'string', nullable: true, readOnly: true }, images: { type: 'array', items: { type: 'string', format: 'uri' } } } }, TicketInput: { type: 'object', required: ['title'], properties: { title: { type: 'string' }, detail: { type: 'string', nullable: true }, type: { type: 'string', enum: TYPES, default: 'backend' }, status: { type: 'string', enum: STATUSES, default: 'open' }, message: { type: 'string', nullable: true }, logs: { type: 'string', nullable: true }, sort: { type: 'integer', default: 0 }, subtic: { type: 'integer', nullable: true }, projectid: { type: 'integer', nullable: true } } }, FlowTicket: { type: 'object', properties: { id: { type: 'integer' }, title: { type: 'string' }, detail: { type: 'string', nullable: true }, status: { type: 'string', enum: STATUSES }, message: { type: 'string', nullable: true } } }, FlowUpdate: { type: 'object', required: ['id'], properties: { id: { type: 'integer' }, status: { type: 'string', enum: STATUSES }, message: { type: 'string' }, detail: { type: 'string' } } }, ChefMenuItem: { type: 'object', properties: { id: { type: 'integer', readOnly: true }, name: { type: 'string' }, price: { type: 'number' }, description: { type: 'string', nullable: true }, creatid: { type: 'string', format: 'date-time', readOnly: true } } }, ChefAddon: { type: 'object', properties: { id: { type: 'integer', readOnly: true }, name: { type: 'string' }, price: { type: 'number' }, creatid: { type: 'string', format: 'date-time', readOnly: true } } }, ChefOrderItem: { type: 'object', properties: { type: { type: 'string', enum: ['food', 'drink', 'addon'] }, id: { type: 'integer' }, name: { type: 'string', readOnly: true }, price: { type: 'number', readOnly: true }, qty: { type: 'integer' } } }, ChefOrder: { type: 'object', properties: { id: { type: 'integer', readOnly: true }, client: { type: 'integer' }, total_item: { type: 'integer', readOnly: true }, total_price: { type: 'integer', readOnly: true }, detail: { type: 'array', items: { $ref: '#/components/schemas/ChefOrderItem' } }, created_at: { type: 'string', format: 'date-time', readOnly: true } } }, ChefOrderInput: { type: 'object', required: ['client', 'items'], properties: { client: { type: 'integer' }, items: { type: 'array', items: { type: 'object', required: ['type', 'id', 'qty'], properties: { type: { type: 'string', enum: ['food', 'drink', 'addon'] }, id: { type: 'integer' }, qty: { type: 'integer', minimum: 1 } } } } } } }, responses: { NotFound: { description: 'Not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } }, BadRequest: { description: 'Invalid request', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } }, ServerError: { description: 'Server error', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } }, paths: { '/api/health': { get: { tags: ['Health'], summary: 'Health check', responses: { 200: { description: 'OK', content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean' } } } } } } } } }, '/api/projects': { get: { tags: ['Projects'], summary: 'List projects', responses: { 200: { description: 'Array of projects', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Project' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } }, post: { tags: ['Projects'], summary: 'Create project', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ProjectInput' } } } }, responses: { 201: { description: 'Created', content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/api/projects/{id}': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } } ], get: { tags: ['Projects'], summary: 'Get project', responses: { 200: { description: 'Project', content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } } }, 404: { $ref: '#/components/responses/NotFound' } } }, put: { tags: ['Projects'], summary: 'Update project', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ProjectInput' } } } }, responses: { 200: { description: 'Updated', content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } } }, 500: { $ref: '#/components/responses/ServerError' } } }, delete: { tags: ['Projects'], summary: 'Delete project', responses: { 204: { description: 'Deleted' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/api/tickets': { get: { tags: ['Tickets'], summary: 'List tickets', parameters: [ { name: 'subtic', in: 'query', schema: { type: 'string' }, description: 'Filter by parent ticket id, or "null" for top-level tickets.' }, { name: 'projectid', in: 'query', schema: { type: 'integer' }, description: 'Filter by project id.' }, { name: 'project_name', in: 'query', schema: { type: 'string' }, description: 'Filter by project name; resolved to the matching project id.' } ], responses: { 200: { description: 'Array of tickets', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } }, post: { tags: ['Tickets'], summary: 'Create ticket', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketInput' } } } }, responses: { 201: { description: 'Created', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/api/tickets/{id}': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } } ], get: { tags: ['Tickets'], summary: 'Get ticket', responses: { 200: { description: 'Ticket', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 404: { $ref: '#/components/responses/NotFound' } } }, put: { tags: ['Tickets'], summary: 'Update ticket', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketInput' } } } }, responses: { 200: { description: 'Updated', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { $ref: '#/components/responses/ServerError' } } }, delete: { tags: ['Tickets'], summary: 'Delete ticket (sub-tickets cascade)', responses: { 204: { description: 'Deleted' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/api/tickets/{id}/images': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } } ], post: { tags: ['Tickets'], summary: 'Upload ticket images', requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { images: { type: 'array', items: { type: 'string', format: 'binary' } } } } } } }, responses: { 201: { description: 'Ticket with appended image URLs', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 404: { $ref: '#/components/responses/NotFound' }, 500: { $ref: '#/components/responses/ServerError' } } }, delete: { tags: ['Tickets'], summary: 'Remove one image URL from a ticket', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['url'], properties: { url: { type: 'string', format: 'uri' } } } } } }, responses: { 200: { description: 'Updated ticket', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 404: { $ref: '#/components/responses/NotFound' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/flow/tickets': { get: { tags: ['Flow'], summary: 'List tickets (id, title, status)', responses: { 200: { description: 'Array of flow tickets', content: { 'application/json': { schema: { type: 'array', items: { type: 'object', properties: { id: { type: 'integer' }, title: { type: 'string' }, status: { type: 'string', enum: STATUSES } } } } } } }, 500: { $ref: '#/components/responses/ServerError' } } }, put: { tags: ['Flow'], summary: 'Update ticket status/message/detail', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/FlowUpdate' } } } }, responses: { 200: { description: 'Updated ticket', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/flow/tickets/queue': { get: { tags: ['Flow'], summary: 'Next queued ticket for a project', description: 'Returns the first ticket for the given project with status "queued", ordered by the smallest sort.', parameters: [ { name: 'project_name', in: 'query', required: true, schema: { type: 'string' }, description: 'Project name; resolved to the matching project id.' } ], responses: { 200: { description: 'The next queued ticket', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 404: { $ref: '#/components/responses/NotFound' }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/flow/tickets/{id}': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } } ], get: { tags: ['Flow'], summary: 'Get ticket (id, title, detail, status, message)', responses: { 200: { description: 'Flow ticket', content: { 'application/json': { schema: { $ref: '#/components/schemas/FlowTicket' } } } }, 404: { $ref: '#/components/responses/NotFound' } } } }, '/chefsys/foods': { get: { tags: ['ChefSys'], summary: 'List foods', responses: { 200: { description: 'Array of foods', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/ChefMenuItem' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/chefsys/drinks': { get: { tags: ['ChefSys'], summary: 'List drinks', responses: { 200: { description: 'Array of drinks', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/ChefMenuItem' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/chefsys/add-on': { get: { tags: ['ChefSys'], summary: 'List add-ons', responses: { 200: { description: 'Array of add-ons', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/ChefAddon' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } } }, '/chefsys/order': { get: { tags: ['ChefSys'], summary: 'List all orders', responses: { 200: { description: 'Array of orders', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/ChefOrder' } } } } }, 500: { $ref: '#/components/responses/ServerError' } } }, post: { tags: ['ChefSys'], summary: 'Create an order', description: 'Item name/price are re-resolved server-side from foods/drinks/add-ons; total is computed from them.', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ChefOrderInput' } } } }, responses: { 201: { description: 'Created', content: { 'application/json': { schema: { $ref: '#/components/schemas/ChefOrder' } } } }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { $ref: '#/components/responses/ServerError' } } } } } };