api / src /routes /flow.js
Tataaicoltd's picture
deploy: update taboard API
8d0f3eb verified
Raw History Blame Contribute Delete
3.81 kB
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=<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;