feat(lanes): expose GET/POST /api/lanes/:id/features over lane-features.js (B)
This commit is contained in:
+51
-1
@@ -13,7 +13,8 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { db } = require("../db");
|
||||
const lanesLib = require("../lib/lanes");
|
||||
const { listPipelines } = require("../lib/pipelines");
|
||||
const { listPipelines, getPipeline, nodeStates, progressPct } = require("../lib/pipelines");
|
||||
const laneFeatures = require("../lib/lane-features");
|
||||
const { broadcast } = require("../websocket");
|
||||
const runs = require("../lib/run-spawner");
|
||||
const { sameOriginGuard } = require("./run");
|
||||
@@ -64,6 +65,18 @@ function payload(lane) {
|
||||
return lanesLib.lanePayload(lane, lastEventAge(lane));
|
||||
}
|
||||
|
||||
/** A feature row's pipeline view, computed the same way payload() computes
|
||||
* it for a live lane — lets the client render an archived feature with the
|
||||
* exact same PipelineMap component, no special-casing on the frontend. */
|
||||
function featurePayload(feature) {
|
||||
const pipeline = getPipeline(feature.pipeline);
|
||||
return {
|
||||
...feature,
|
||||
pipeline_name: pipeline.name,
|
||||
pipeline_nodes: nodeStates(pipeline, feature),
|
||||
progress: progressPct(pipeline, feature),
|
||||
};
|
||||
}
|
||||
/** Push the current state of one lane to every connected client. */
|
||||
function broadcastLane(id) {
|
||||
const lane = lanesLib.getLane(id);
|
||||
@@ -240,6 +253,43 @@ router.get("/:id/preflight", async (req, res) => {
|
||||
* POST and Express matches on method as well as path. Verified by moving this
|
||||
* registration after it: the suite stayed green.
|
||||
*/
|
||||
router.get("/:id/features", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
res.json({ features: laneFeatures.listFeatures(lane.id).map(featurePayload) });
|
||||
});
|
||||
|
||||
router.get("/:id/features/:slug", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
const feature = laneFeatures.getFeature(lane.id, req.params.slug);
|
||||
if (!feature) {
|
||||
return res.status(404).json({ error: { code: "ENOFEATURE", message: "no such feature" } });
|
||||
}
|
||||
res.json({ feature: featurePayload(feature) });
|
||||
});
|
||||
|
||||
router.post("/:id/features/activate", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
const slug = typeof req.body?.slug === "string" ? req.body.slug : "";
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: { code: "EBADSLUG", message: "slug is required" } });
|
||||
}
|
||||
try {
|
||||
const { lane: updated, feature } = laneFeatures.activateFeature(lane.id, slug, {
|
||||
title: req.body?.title,
|
||||
});
|
||||
broadcastLane(updated.id);
|
||||
res.json({ lane: payload(updated), feature: featurePayload(feature) });
|
||||
} catch (err) {
|
||||
if (err.code === "EBADSLUG") {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
res.status(500).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id/git", async (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
|
||||
Reference in New Issue
Block a user