import { Router } from 'express'; import { supabase } from '../supabase.js'; import { STATUSES, statusToInt, intToStatus } from '../status.js'; // Lightweight "flow" API for agents. Mounted at /flow/tickets. const router = Router(); const TABLE = 'ztickets'; // DB stores images as text (JSON array of URLs); expose as an array. function parseImages(v) { if (!v) return []; if (Array.isArray(v)) return v; try { const a = JSON.parse(v); return Array.isArray(a) ? a : []; } catch { return String(v).split(/[\n,]+/).map((s) => s.trim()).filter(Boolean); } } // GET /flow/tickets -> [{ id, title, status }] router.get('/', async (req, res) => { const { data, error } = await supabase .from(TABLE) .select('id, title, status') .order('sort', { ascending: true }) .order('id', { ascending: true }); if (error) return res.status(500).json({ error: error.message }); res.json(data.map((t) => ({ id: t.id, title: t.title, status: intToStatus(t.status) }))); }); // GET /flow/tickets/queue?project_name= // Returns the next queued ticket for a project: status = 'queued', lowest sort first. // Defined before '/:id' so the literal path is not treated as an id. router.get('/queue', async (req, res) => { const projectName = req.query.project_name; if (!projectName) return res.status(400).json({ error: 'project_name is required' }); const { data: project, error: projErr } = await supabase .from('zprojects') .select('id, name') .eq('name', projectName) .maybeSingle(); if (projErr) return res.status(500).json({ error: projErr.message }); if (!project) return res.status(404).json({ error: `project not found: ${projectName}` }); const { data, error } = await supabase .from(TABLE) .select('*') .eq('projectid', project.id) .eq('status', statusToInt('queued')) .order('sort', { ascending: true }) .order('id', { ascending: true }) .limit(1) .maybeSingle(); if (error) return res.status(500).json({ error: error.message }); if (!data) return res.status(404).json({ error: 'no queued ticket found' }); res.json({ ...data, status: intToStatus(data.status), images: parseImages(data.images), project_name: project.name }); }); // GET /flow/tickets/:id -> { id, title, detail, status, message } router.get('/:id', async (req, res) => { const { data, error } = await supabase .from(TABLE) .select('id, title, detail, status, message') .eq('id', req.params.id) .single(); if (error) return res.status(404).json({ error: error.message }); res.json({ id: data.id, title: data.title, detail: data.detail, status: intToStatus(data.status), message: data.message }); }); // PUT /flow/tickets body: { id, status?, message?, detail? } -> updated ticket router.put('/', async (req, res) => { const { id, status, message, detail } = req.body; if (id === undefined || id === null || id === '') { return res.status(400).json({ error: 'id is required' }); } const payload = {}; if (status !== undefined) { if (!STATUSES.includes(status)) { return res.status(400).json({ error: `status must be one of ${STATUSES.join(', ')}` }); } payload.status = statusToInt(status); } if (message !== undefined) payload.message = message; if (detail !== undefined) payload.detail = detail; if (Object.keys(payload).length === 0) { return res.status(400).json({ error: 'nothing to update (provide status, message or detail)' }); } const { data, error } = await supabase .from(TABLE) .update(payload) .eq('id', id) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json({ ...data, status: intToStatus(data.status) }); }); export default router;