@unbrained/pm-web 2026.6.4 → 2026.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/board.js +27 -7
- package/dist/board.js.map +1 -1
- package/dist/routes/admin.js +11 -10
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/github.js +19 -18
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/groups.js +11 -10
- package/dist/routes/groups.js.map +1 -1
- package/dist/routes/pm.js +199 -198
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +4 -3
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/route-params.js +7 -0
- package/dist/routes/route-params.js.map +1 -0
- package/dist/routes/sharing.js +5 -4
- package/dist/routes/sharing.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +13 -13
- package/public/src/utils.js +3 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +3 -1
package/dist/routes/pm.js
CHANGED
|
@@ -7,6 +7,7 @@ import { verifyProjectAccess } from "./projects.js";
|
|
|
7
7
|
import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, broadcastPresence, updateClientView, getProjectPresence } from "../services/sse.js";
|
|
8
8
|
import { v4 as uuidv4 } from "uuid";
|
|
9
9
|
import neo4j from "neo4j-driver";
|
|
10
|
+
import { routeParam } from "./route-params.js";
|
|
10
11
|
// Singleton Neo4j driver — reused across sync calls to avoid per-call connection overhead.
|
|
11
12
|
let _neo4jDriver = null;
|
|
12
13
|
let _neo4jDriverKey = "";
|
|
@@ -32,7 +33,7 @@ router.use(async (req, res, next) => {
|
|
|
32
33
|
next();
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const projectId = req
|
|
36
|
+
const projectId = routeParam(req, "projectId");
|
|
36
37
|
if (!projectId) {
|
|
37
38
|
next();
|
|
38
39
|
return;
|
|
@@ -335,7 +336,7 @@ async function verifyProject(userId, projectId) {
|
|
|
335
336
|
// Returns runtime types/statuses from `pm contracts --json` so the frontend
|
|
336
337
|
// stays in sync with whatever pm CLI version + extensions are installed.
|
|
337
338
|
router.get("/schema", async (req, res) => {
|
|
338
|
-
const project = await verifyProject(req.user.userId, req
|
|
339
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
339
340
|
if (!project) {
|
|
340
341
|
res.status(404).json({ error: "Project not found" });
|
|
341
342
|
return;
|
|
@@ -353,7 +354,7 @@ router.get("/schema", async (req, res) => {
|
|
|
353
354
|
});
|
|
354
355
|
// GET /api/projects/:projectId/pm/list
|
|
355
356
|
router.get("/list", async (req, res) => {
|
|
356
|
-
const project = await verifyProject(req.user.userId, req
|
|
357
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
357
358
|
if (!project) {
|
|
358
359
|
res.status(404).json({ error: "Project not found" });
|
|
359
360
|
return;
|
|
@@ -379,7 +380,7 @@ router.get("/list", async (req, res) => {
|
|
|
379
380
|
});
|
|
380
381
|
// GET /api/projects/:projectId/pm/list-all
|
|
381
382
|
router.get("/list-all", async (req, res) => {
|
|
382
|
-
const project = await verifyProject(req.user.userId, req
|
|
383
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
383
384
|
if (!project) {
|
|
384
385
|
res.status(404).json({ error: "Project not found" });
|
|
385
386
|
return;
|
|
@@ -397,7 +398,7 @@ router.get("/list-all", async (req, res) => {
|
|
|
397
398
|
// Kanban board: items grouped into columns by the workspace's runtime statuses
|
|
398
399
|
// (read live from `pm contracts`) so the board matches the installed CLI.
|
|
399
400
|
router.get("/board", async (req, res) => {
|
|
400
|
-
const project = await verifyProject(req.user.userId, req
|
|
401
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
401
402
|
if (!project) {
|
|
402
403
|
res.status(404).json({ error: "Project not found" });
|
|
403
404
|
return;
|
|
@@ -418,7 +419,7 @@ router.get("/board", async (req, res) => {
|
|
|
418
419
|
// GET /api/projects/:projectId/pm/search?q=...
|
|
419
420
|
// Full-text search over id/title/tags/body via a single list-all read.
|
|
420
421
|
router.get("/search", async (req, res) => {
|
|
421
|
-
const project = await verifyProject(req.user.userId, req
|
|
422
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
422
423
|
if (!project) {
|
|
423
424
|
res.status(404).json({ error: "Project not found" });
|
|
424
425
|
return;
|
|
@@ -434,7 +435,7 @@ router.get("/search", async (req, res) => {
|
|
|
434
435
|
});
|
|
435
436
|
// POST /api/projects/:projectId/pm/create
|
|
436
437
|
router.post("/create", async (req, res) => {
|
|
437
|
-
const project = await verifyProject(req.user.userId, req
|
|
438
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
438
439
|
if (!project) {
|
|
439
440
|
res.status(404).json({ error: "Project not found" });
|
|
440
441
|
return;
|
|
@@ -513,22 +514,22 @@ router.post("/create", async (req, res) => {
|
|
|
513
514
|
return;
|
|
514
515
|
}
|
|
515
516
|
// Broadcast SSE create event
|
|
516
|
-
broadcastProjectEvent(req
|
|
517
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
517
518
|
type: "item-created",
|
|
518
519
|
data: { result: result.parsed, userId: req.user.userId },
|
|
519
520
|
});
|
|
520
|
-
scheduleGraphSync(req
|
|
521
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-created");
|
|
521
522
|
res.status(201).json(result.parsed || {});
|
|
522
523
|
});
|
|
523
524
|
// GET /api/projects/:projectId/pm/get/:itemId
|
|
524
525
|
router.get("/get/:itemId", async (req, res) => {
|
|
525
|
-
const project = await verifyProject(req.user.userId, req
|
|
526
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
526
527
|
if (!project) {
|
|
527
528
|
res.status(404).json({ error: "Project not found" });
|
|
528
529
|
return;
|
|
529
530
|
}
|
|
530
531
|
const result = runPm({
|
|
531
|
-
args: ["get", req
|
|
532
|
+
args: ["get", routeParam(req, "itemId")],
|
|
532
533
|
userId: project.ownerUserId,
|
|
533
534
|
slug: project.slug,
|
|
534
535
|
jsonOutput: true,
|
|
@@ -541,13 +542,13 @@ router.get("/get/:itemId", async (req, res) => {
|
|
|
541
542
|
});
|
|
542
543
|
// PATCH /api/projects/:projectId/pm/update/:itemId
|
|
543
544
|
router.patch("/update/:itemId", async (req, res) => {
|
|
544
|
-
const project = await verifyProject(req.user.userId, req
|
|
545
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
545
546
|
if (!project) {
|
|
546
547
|
res.status(404).json({ error: "Project not found" });
|
|
547
548
|
return;
|
|
548
549
|
}
|
|
549
550
|
const body = req.body;
|
|
550
|
-
const args = ["update", req
|
|
551
|
+
const args = ["update", routeParam(req, "itemId")];
|
|
551
552
|
// String options
|
|
552
553
|
const stringFlags = {
|
|
553
554
|
title: "--title", description: "--description", status: "--status", priority: "--priority",
|
|
@@ -580,16 +581,16 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
580
581
|
return;
|
|
581
582
|
}
|
|
582
583
|
// Broadcast SSE update event
|
|
583
|
-
broadcastProjectEvent(req
|
|
584
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
584
585
|
type: "item-updated",
|
|
585
|
-
data: { itemId: req
|
|
586
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
586
587
|
});
|
|
587
|
-
scheduleGraphSync(req
|
|
588
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-updated");
|
|
588
589
|
res.json(result.parsed || {});
|
|
589
590
|
});
|
|
590
591
|
// POST /api/projects/:projectId/pm/close/:itemId
|
|
591
592
|
router.post("/close/:itemId", async (req, res) => {
|
|
592
|
-
const project = await verifyProject(req.user.userId, req
|
|
593
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
593
594
|
if (!project) {
|
|
594
595
|
res.status(404).json({ error: "Project not found" });
|
|
595
596
|
return;
|
|
@@ -600,7 +601,7 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
600
601
|
return;
|
|
601
602
|
}
|
|
602
603
|
const result = runPm({
|
|
603
|
-
args: ["close", req
|
|
604
|
+
args: ["close", routeParam(req, "itemId"), reason.trim()],
|
|
604
605
|
userId: project.ownerUserId,
|
|
605
606
|
slug: project.slug,
|
|
606
607
|
jsonOutput: true,
|
|
@@ -609,22 +610,22 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
609
610
|
res.status(400).json({ error: result.stderr || "Failed to close item" });
|
|
610
611
|
return;
|
|
611
612
|
}
|
|
612
|
-
broadcastProjectEvent(req
|
|
613
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
613
614
|
type: "item-closed",
|
|
614
|
-
data: { itemId: req
|
|
615
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
615
616
|
});
|
|
616
|
-
scheduleGraphSync(req
|
|
617
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-closed");
|
|
617
618
|
res.json(result.parsed || {});
|
|
618
619
|
});
|
|
619
620
|
// DELETE /api/projects/:projectId/pm/delete/:itemId
|
|
620
621
|
router.delete("/delete/:itemId", async (req, res) => {
|
|
621
|
-
const project = await verifyProject(req.user.userId, req
|
|
622
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
622
623
|
if (!project) {
|
|
623
624
|
res.status(404).json({ error: "Project not found" });
|
|
624
625
|
return;
|
|
625
626
|
}
|
|
626
627
|
const result = runPm({
|
|
627
|
-
args: ["delete", req
|
|
628
|
+
args: ["delete", routeParam(req, "itemId"), "--yes"],
|
|
628
629
|
userId: project.ownerUserId,
|
|
629
630
|
slug: project.slug,
|
|
630
631
|
});
|
|
@@ -632,16 +633,16 @@ router.delete("/delete/:itemId", async (req, res) => {
|
|
|
632
633
|
res.status(400).json({ error: result.stderr || "Failed to delete item" });
|
|
633
634
|
return;
|
|
634
635
|
}
|
|
635
|
-
broadcastProjectEvent(req
|
|
636
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
636
637
|
type: "item-deleted",
|
|
637
|
-
data: { itemId: req
|
|
638
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
638
639
|
});
|
|
639
|
-
scheduleGraphSync(req
|
|
640
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-deleted");
|
|
640
641
|
res.json({ ok: true });
|
|
641
642
|
});
|
|
642
643
|
// POST /api/projects/:projectId/pm/comments/:itemId
|
|
643
644
|
router.post("/comments/:itemId", async (req, res) => {
|
|
644
|
-
const project = await verifyProject(req.user.userId, req
|
|
645
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
645
646
|
if (!project) {
|
|
646
647
|
res.status(404).json({ error: "Project not found" });
|
|
647
648
|
return;
|
|
@@ -652,7 +653,7 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
652
653
|
return;
|
|
653
654
|
}
|
|
654
655
|
const result = runPm({
|
|
655
|
-
args: ["comments", req
|
|
656
|
+
args: ["comments", routeParam(req, "itemId"), text.trim()],
|
|
656
657
|
userId: project.ownerUserId,
|
|
657
658
|
slug: project.slug,
|
|
658
659
|
jsonOutput: true,
|
|
@@ -665,13 +666,13 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
665
666
|
});
|
|
666
667
|
// GET /api/projects/:projectId/pm/comments/:itemId
|
|
667
668
|
router.get("/comments/:itemId", async (req, res) => {
|
|
668
|
-
const project = await verifyProject(req.user.userId, req
|
|
669
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
669
670
|
if (!project) {
|
|
670
671
|
res.status(404).json({ error: "Project not found" });
|
|
671
672
|
return;
|
|
672
673
|
}
|
|
673
674
|
const result = runPm({
|
|
674
|
-
args: ["comments", req
|
|
675
|
+
args: ["comments", routeParam(req, "itemId")],
|
|
675
676
|
userId: project.ownerUserId,
|
|
676
677
|
slug: project.slug,
|
|
677
678
|
jsonOutput: true,
|
|
@@ -680,13 +681,13 @@ router.get("/comments/:itemId", async (req, res) => {
|
|
|
680
681
|
});
|
|
681
682
|
// GET /api/projects/:projectId/pm/notes/:itemId
|
|
682
683
|
router.get("/notes/:itemId", async (req, res) => {
|
|
683
|
-
const project = await verifyProject(req.user.userId, req
|
|
684
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
684
685
|
if (!project) {
|
|
685
686
|
res.status(404).json({ error: "Project not found" });
|
|
686
687
|
return;
|
|
687
688
|
}
|
|
688
689
|
const result = runPm({
|
|
689
|
-
args: ["notes", req
|
|
690
|
+
args: ["notes", routeParam(req, "itemId")],
|
|
690
691
|
userId: project.ownerUserId,
|
|
691
692
|
slug: project.slug,
|
|
692
693
|
jsonOutput: true,
|
|
@@ -695,7 +696,7 @@ router.get("/notes/:itemId", async (req, res) => {
|
|
|
695
696
|
});
|
|
696
697
|
// POST /api/projects/:projectId/pm/notes/:itemId
|
|
697
698
|
router.post("/notes/:itemId", async (req, res) => {
|
|
698
|
-
const project = await verifyProject(req.user.userId, req
|
|
699
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
699
700
|
if (!project) {
|
|
700
701
|
res.status(404).json({ error: "Project not found" });
|
|
701
702
|
return;
|
|
@@ -706,7 +707,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
706
707
|
return;
|
|
707
708
|
}
|
|
708
709
|
const result = runPm({
|
|
709
|
-
args: ["notes", req
|
|
710
|
+
args: ["notes", routeParam(req, "itemId"), text.trim()],
|
|
710
711
|
userId: project.ownerUserId,
|
|
711
712
|
slug: project.slug,
|
|
712
713
|
jsonOutput: true,
|
|
@@ -719,7 +720,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
719
720
|
});
|
|
720
721
|
// GET /api/projects/:projectId/pm/context
|
|
721
722
|
router.get("/context", async (req, res) => {
|
|
722
|
-
const project = await verifyProject(req.user.userId, req
|
|
723
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
723
724
|
if (!project) {
|
|
724
725
|
res.status(404).json({ error: "Project not found" });
|
|
725
726
|
return;
|
|
@@ -737,7 +738,7 @@ router.get("/context", async (req, res) => {
|
|
|
737
738
|
});
|
|
738
739
|
// GET /api/projects/:projectId/pm/activity
|
|
739
740
|
router.get("/activity", async (req, res) => {
|
|
740
|
-
const project = await verifyProject(req.user.userId, req
|
|
741
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
741
742
|
if (!project) {
|
|
742
743
|
res.status(404).json({ error: "Project not found" });
|
|
743
744
|
return;
|
|
@@ -751,7 +752,7 @@ router.get("/activity", async (req, res) => {
|
|
|
751
752
|
});
|
|
752
753
|
// GET /api/projects/:projectId/pm/stats
|
|
753
754
|
router.get("/stats", async (req, res) => {
|
|
754
|
-
const project = await verifyProject(req.user.userId, req
|
|
755
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
755
756
|
if (!project) {
|
|
756
757
|
res.status(404).json({ error: "Project not found" });
|
|
757
758
|
return;
|
|
@@ -766,7 +767,7 @@ router.get("/stats", async (req, res) => {
|
|
|
766
767
|
});
|
|
767
768
|
// GET /api/projects/:projectId/pm/aggregate
|
|
768
769
|
router.get("/aggregate", async (req, res) => {
|
|
769
|
-
const project = await verifyProject(req.user.userId, req
|
|
770
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
770
771
|
if (!project) {
|
|
771
772
|
res.status(404).json({ error: "Project not found" });
|
|
772
773
|
return;
|
|
@@ -781,7 +782,7 @@ router.get("/aggregate", async (req, res) => {
|
|
|
781
782
|
});
|
|
782
783
|
// POST /api/projects/:projectId/pm/search
|
|
783
784
|
router.post("/search", async (req, res) => {
|
|
784
|
-
const project = await verifyProject(req.user.userId, req
|
|
785
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
785
786
|
if (!project) {
|
|
786
787
|
res.status(404).json({ error: "Project not found" });
|
|
787
788
|
return;
|
|
@@ -810,7 +811,7 @@ router.post("/search", async (req, res) => {
|
|
|
810
811
|
});
|
|
811
812
|
// GET /api/projects/:projectId/pm/calendar
|
|
812
813
|
router.get("/calendar", async (req, res) => {
|
|
813
|
-
const project = await verifyProject(req.user.userId, req
|
|
814
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
814
815
|
if (!project) {
|
|
815
816
|
res.status(404).json({ error: "Project not found" });
|
|
816
817
|
return;
|
|
@@ -829,7 +830,7 @@ router.get("/calendar", async (req, res) => {
|
|
|
829
830
|
// calendar view. Auth works via the usual token (header/cookie) or a
|
|
830
831
|
// `?token=` query param, since calendar clients cannot send cookies.
|
|
831
832
|
router.get("/calendar.ics", async (req, res) => {
|
|
832
|
-
const project = await verifyProject(req.user.userId, req
|
|
833
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
833
834
|
if (!project) {
|
|
834
835
|
res.status(404).json({ error: "Project not found" });
|
|
835
836
|
return;
|
|
@@ -867,7 +868,7 @@ router.get("/calendar.ics", async (req, res) => {
|
|
|
867
868
|
});
|
|
868
869
|
// GET /api/projects/:projectId/pm/health
|
|
869
870
|
router.get("/health", async (req, res) => {
|
|
870
|
-
const project = await verifyProject(req.user.userId, req
|
|
871
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
871
872
|
if (!project) {
|
|
872
873
|
res.status(404).json({ error: "Project not found" });
|
|
873
874
|
return;
|
|
@@ -882,7 +883,7 @@ router.get("/health", async (req, res) => {
|
|
|
882
883
|
});
|
|
883
884
|
// POST /api/projects/:projectId/pm/append/:itemId
|
|
884
885
|
router.post("/append/:itemId", async (req, res) => {
|
|
885
|
-
const project = await verifyProject(req.user.userId, req
|
|
886
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
886
887
|
if (!project) {
|
|
887
888
|
res.status(404).json({ error: "Project not found" });
|
|
888
889
|
return;
|
|
@@ -893,7 +894,7 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
893
894
|
return;
|
|
894
895
|
}
|
|
895
896
|
const result = runPm({
|
|
896
|
-
args: ["append", req
|
|
897
|
+
args: ["append", routeParam(req, "itemId"), text.trim()],
|
|
897
898
|
userId: project.ownerUserId,
|
|
898
899
|
slug: project.slug,
|
|
899
900
|
jsonOutput: true,
|
|
@@ -902,18 +903,18 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
902
903
|
res.status(400).json({ error: result.stderr || "Failed to append" });
|
|
903
904
|
return;
|
|
904
905
|
}
|
|
905
|
-
scheduleGraphSync(req
|
|
906
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-appended");
|
|
906
907
|
res.json(result.parsed || { ok: true });
|
|
907
908
|
});
|
|
908
909
|
// GET /api/projects/:projectId/pm/history/:itemId
|
|
909
910
|
router.get("/history/:itemId", async (req, res) => {
|
|
910
|
-
const project = await verifyProject(req.user.userId, req
|
|
911
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
911
912
|
if (!project) {
|
|
912
913
|
res.status(404).json({ error: "Project not found" });
|
|
913
914
|
return;
|
|
914
915
|
}
|
|
915
916
|
const result = runPm({
|
|
916
|
-
args: ["history", req
|
|
917
|
+
args: ["history", routeParam(req, "itemId")],
|
|
917
918
|
userId: project.ownerUserId,
|
|
918
919
|
slug: project.slug,
|
|
919
920
|
jsonOutput: true,
|
|
@@ -922,13 +923,13 @@ router.get("/history/:itemId", async (req, res) => {
|
|
|
922
923
|
});
|
|
923
924
|
// GET /api/projects/:projectId/pm/deps/:itemId
|
|
924
925
|
router.get("/deps/:itemId", async (req, res) => {
|
|
925
|
-
const project = await verifyProject(req.user.userId, req
|
|
926
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
926
927
|
if (!project) {
|
|
927
928
|
res.status(404).json({ error: "Project not found" });
|
|
928
929
|
return;
|
|
929
930
|
}
|
|
930
931
|
const result = runPm({
|
|
931
|
-
args: ["deps", req
|
|
932
|
+
args: ["deps", routeParam(req, "itemId")],
|
|
932
933
|
userId: project.ownerUserId,
|
|
933
934
|
slug: project.slug,
|
|
934
935
|
jsonOutput: true,
|
|
@@ -937,7 +938,7 @@ router.get("/deps/:itemId", async (req, res) => {
|
|
|
937
938
|
});
|
|
938
939
|
// POST /api/projects/:projectId/pm/deps/:itemId
|
|
939
940
|
router.post("/deps/:itemId", async (req, res) => {
|
|
940
|
-
const project = await verifyProject(req.user.userId, req
|
|
941
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
941
942
|
if (!project) {
|
|
942
943
|
res.status(404).json({ error: "Project not found" });
|
|
943
944
|
return;
|
|
@@ -949,7 +950,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
949
950
|
}
|
|
950
951
|
const depRel = normalizeDependencyKind(rel);
|
|
951
952
|
const result = runPm({
|
|
952
|
-
args: ["update", req
|
|
953
|
+
args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
|
|
953
954
|
userId: project.ownerUserId,
|
|
954
955
|
slug: project.slug,
|
|
955
956
|
jsonOutput: true,
|
|
@@ -958,9 +959,9 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
958
959
|
res.status(400).json({ error: result.stderr || "Failed to add dependency" });
|
|
959
960
|
return;
|
|
960
961
|
}
|
|
961
|
-
scheduleGraphSync(req
|
|
962
|
-
broadcastDependencyEvent(req
|
|
963
|
-
from: req
|
|
962
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-added");
|
|
963
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
964
|
+
from: routeParam(req, "itemId"),
|
|
964
965
|
to: targetId.trim(),
|
|
965
966
|
rel: depRel,
|
|
966
967
|
userId: req.user.userId,
|
|
@@ -969,7 +970,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
969
970
|
});
|
|
970
971
|
// DELETE /api/projects/:projectId/pm/deps/:itemId
|
|
971
972
|
router.delete("/deps/:itemId", async (req, res) => {
|
|
972
|
-
const project = await verifyProject(req.user.userId, req
|
|
973
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
973
974
|
if (!project) {
|
|
974
975
|
res.status(404).json({ error: "Project not found" });
|
|
975
976
|
return;
|
|
@@ -982,7 +983,7 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
982
983
|
const depRel = normalizeDependencyKind(rel || "relates_to");
|
|
983
984
|
const selector = `id=${targetId.trim()},kind=${depRel}`;
|
|
984
985
|
const result = runPm({
|
|
985
|
-
args: ["update", req
|
|
986
|
+
args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
|
|
986
987
|
userId: project.ownerUserId,
|
|
987
988
|
slug: project.slug,
|
|
988
989
|
jsonOutput: true,
|
|
@@ -991,18 +992,18 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
991
992
|
res.status(400).json({ error: result.stderr || "Failed to remove dependency" });
|
|
992
993
|
return;
|
|
993
994
|
}
|
|
994
|
-
scheduleGraphSync(req
|
|
995
|
-
broadcastDependencyEvent(req
|
|
996
|
-
from: req
|
|
995
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-removed");
|
|
996
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
997
|
+
from: routeParam(req, "itemId"),
|
|
997
998
|
to: targetId.trim(),
|
|
998
999
|
rel: depRel,
|
|
999
1000
|
userId: req.user.userId,
|
|
1000
1001
|
});
|
|
1001
|
-
res.status(200).json({ ok: true, from: req
|
|
1002
|
+
res.status(200).json({ ok: true, from: routeParam(req, "itemId"), to: targetId.trim(), type: depRel, result: result.parsed || null });
|
|
1002
1003
|
});
|
|
1003
1004
|
// POST /api/projects/:projectId/pm/rel — Create a relationship between two items
|
|
1004
1005
|
router.post("/rel", async (req, res) => {
|
|
1005
|
-
const project = await verifyProject(req.user.userId, req
|
|
1006
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1006
1007
|
if (!project) {
|
|
1007
1008
|
res.status(404).json({ error: "Project not found" });
|
|
1008
1009
|
return;
|
|
@@ -1023,8 +1024,8 @@ router.post("/rel", async (req, res) => {
|
|
|
1023
1024
|
res.status(400).json({ error: result.stderr || "Failed to create relationship" });
|
|
1024
1025
|
return;
|
|
1025
1026
|
}
|
|
1026
|
-
scheduleGraphSync(req
|
|
1027
|
-
broadcastDependencyEvent(req
|
|
1027
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-created");
|
|
1028
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
1028
1029
|
from: from.trim(),
|
|
1029
1030
|
to: to.trim(),
|
|
1030
1031
|
rel: depRel,
|
|
@@ -1034,7 +1035,7 @@ router.post("/rel", async (req, res) => {
|
|
|
1034
1035
|
});
|
|
1035
1036
|
// DELETE /api/projects/:projectId/pm/rel — Remove a relationship between two items
|
|
1036
1037
|
router.delete("/rel", async (req, res) => {
|
|
1037
|
-
const project = await verifyProject(req.user.userId, req
|
|
1038
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1038
1039
|
if (!project) {
|
|
1039
1040
|
res.status(404).json({ error: "Project not found" });
|
|
1040
1041
|
return;
|
|
@@ -1056,8 +1057,8 @@ router.delete("/rel", async (req, res) => {
|
|
|
1056
1057
|
res.status(400).json({ error: result.stderr || "Failed to remove relationship" });
|
|
1057
1058
|
return;
|
|
1058
1059
|
}
|
|
1059
|
-
scheduleGraphSync(req
|
|
1060
|
-
broadcastDependencyEvent(req
|
|
1060
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-removed");
|
|
1061
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
1061
1062
|
from: from.trim(),
|
|
1062
1063
|
to: to.trim(),
|
|
1063
1064
|
rel: depRel,
|
|
@@ -1067,7 +1068,7 @@ router.delete("/rel", async (req, res) => {
|
|
|
1067
1068
|
});
|
|
1068
1069
|
// GET /api/projects/:projectId/pm/graph
|
|
1069
1070
|
router.get("/graph", async (req, res) => {
|
|
1070
|
-
const project = await verifyProject(req.user.userId, req
|
|
1071
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1071
1072
|
if (!project) {
|
|
1072
1073
|
res.status(404).json({ error: "Project not found" });
|
|
1073
1074
|
return;
|
|
@@ -1095,7 +1096,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1095
1096
|
});
|
|
1096
1097
|
// POST /api/projects/:projectId/pm/graph/sync
|
|
1097
1098
|
router.post("/graph/sync", async (req, res) => {
|
|
1098
|
-
const project = await verifyProject(req.user.userId, req
|
|
1099
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1099
1100
|
if (!project) {
|
|
1100
1101
|
res.status(404).json({ error: "Project not found" });
|
|
1101
1102
|
return;
|
|
@@ -1103,21 +1104,21 @@ router.post("/graph/sync", async (req, res) => {
|
|
|
1103
1104
|
try {
|
|
1104
1105
|
const syncResult = await syncProjectGraph(project);
|
|
1105
1106
|
const payload = { reason: "manual-sync", ...syncResult, projectKey: graphProjectKey(project), source: "pm-web" };
|
|
1106
|
-
broadcastProjectEvent(req
|
|
1107
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1107
1108
|
type: "graph-synced",
|
|
1108
1109
|
data: payload,
|
|
1109
1110
|
});
|
|
1110
1111
|
res.json({ ok: true, ...payload });
|
|
1111
1112
|
}
|
|
1112
1113
|
catch (err) {
|
|
1113
|
-
broadcastProjectEvent(req
|
|
1114
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1114
1115
|
type: "graph-sync-failed",
|
|
1115
1116
|
data: {
|
|
1116
1117
|
reason: "manual-sync",
|
|
1117
1118
|
error: err instanceof Error ? err.message : String(err),
|
|
1118
1119
|
},
|
|
1119
1120
|
});
|
|
1120
|
-
broadcastProjectEvent(req
|
|
1121
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1121
1122
|
type: "graph_sync_failed",
|
|
1122
1123
|
data: {
|
|
1123
1124
|
reason: "manual-sync",
|
|
@@ -1129,12 +1130,12 @@ router.post("/graph/sync", async (req, res) => {
|
|
|
1129
1130
|
});
|
|
1130
1131
|
// GET /api/projects/:projectId/pm/graph/neighbors/:nodeId
|
|
1131
1132
|
router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
1132
|
-
const project = await verifyProject(req.user.userId, req
|
|
1133
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1133
1134
|
if (!project) {
|
|
1134
1135
|
res.status(404).json({ error: "Project not found" });
|
|
1135
1136
|
return;
|
|
1136
1137
|
}
|
|
1137
|
-
const nodeId = req
|
|
1138
|
+
const nodeId = routeParam(req, "nodeId");
|
|
1138
1139
|
if (!nodeId) {
|
|
1139
1140
|
res.status(400).json({ error: "nodeId is required" });
|
|
1140
1141
|
return;
|
|
@@ -1160,7 +1161,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
|
1160
1161
|
});
|
|
1161
1162
|
// POST /api/projects/:projectId/pm/graph/query
|
|
1162
1163
|
router.post("/graph/query", async (req, res) => {
|
|
1163
|
-
const project = await verifyProject(req.user.userId, req
|
|
1164
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1164
1165
|
if (!project) {
|
|
1165
1166
|
res.status(404).json({ error: "Project not found" });
|
|
1166
1167
|
return;
|
|
@@ -1190,13 +1191,13 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1190
1191
|
});
|
|
1191
1192
|
// GET /api/projects/:projectId/pm/learnings/:itemId
|
|
1192
1193
|
router.get("/learnings/:itemId", async (req, res) => {
|
|
1193
|
-
const project = await verifyProject(req.user.userId, req
|
|
1194
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1194
1195
|
if (!project) {
|
|
1195
1196
|
res.status(404).json({ error: "Project not found" });
|
|
1196
1197
|
return;
|
|
1197
1198
|
}
|
|
1198
1199
|
const result = runPm({
|
|
1199
|
-
args: ["learnings", req
|
|
1200
|
+
args: ["learnings", routeParam(req, "itemId")],
|
|
1200
1201
|
userId: project.ownerUserId,
|
|
1201
1202
|
slug: project.slug,
|
|
1202
1203
|
jsonOutput: true,
|
|
@@ -1205,7 +1206,7 @@ router.get("/learnings/:itemId", async (req, res) => {
|
|
|
1205
1206
|
});
|
|
1206
1207
|
// POST /api/projects/:projectId/pm/learnings/:itemId
|
|
1207
1208
|
router.post("/learnings/:itemId", async (req, res) => {
|
|
1208
|
-
const project = await verifyProject(req.user.userId, req
|
|
1209
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1209
1210
|
if (!project) {
|
|
1210
1211
|
res.status(404).json({ error: "Project not found" });
|
|
1211
1212
|
return;
|
|
@@ -1216,7 +1217,7 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1216
1217
|
return;
|
|
1217
1218
|
}
|
|
1218
1219
|
const result = runPm({
|
|
1219
|
-
args: ["learnings", req
|
|
1220
|
+
args: ["learnings", routeParam(req, "itemId"), text.trim()],
|
|
1220
1221
|
userId: project.ownerUserId,
|
|
1221
1222
|
slug: project.slug,
|
|
1222
1223
|
jsonOutput: true,
|
|
@@ -1229,13 +1230,13 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1229
1230
|
});
|
|
1230
1231
|
// POST /api/projects/:projectId/pm/claim/:itemId
|
|
1231
1232
|
router.post("/claim/:itemId", async (req, res) => {
|
|
1232
|
-
const project = await verifyProject(req.user.userId, req
|
|
1233
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1233
1234
|
if (!project) {
|
|
1234
1235
|
res.status(404).json({ error: "Project not found" });
|
|
1235
1236
|
return;
|
|
1236
1237
|
}
|
|
1237
1238
|
const result = runPm({
|
|
1238
|
-
args: ["claim", req
|
|
1239
|
+
args: ["claim", routeParam(req, "itemId")],
|
|
1239
1240
|
userId: project.ownerUserId,
|
|
1240
1241
|
slug: project.slug,
|
|
1241
1242
|
jsonOutput: true,
|
|
@@ -1244,18 +1245,18 @@ router.post("/claim/:itemId", async (req, res) => {
|
|
|
1244
1245
|
res.status(400).json({ error: result.stderr || "Failed to claim item" });
|
|
1245
1246
|
return;
|
|
1246
1247
|
}
|
|
1247
|
-
scheduleGraphSync(req
|
|
1248
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-claimed");
|
|
1248
1249
|
res.json(result.parsed || { ok: true });
|
|
1249
1250
|
});
|
|
1250
1251
|
// POST /api/projects/:projectId/pm/release/:itemId
|
|
1251
1252
|
router.post("/release/:itemId", async (req, res) => {
|
|
1252
|
-
const project = await verifyProject(req.user.userId, req
|
|
1253
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1253
1254
|
if (!project) {
|
|
1254
1255
|
res.status(404).json({ error: "Project not found" });
|
|
1255
1256
|
return;
|
|
1256
1257
|
}
|
|
1257
1258
|
const result = runPm({
|
|
1258
|
-
args: ["release", req
|
|
1259
|
+
args: ["release", routeParam(req, "itemId")],
|
|
1259
1260
|
userId: project.ownerUserId,
|
|
1260
1261
|
slug: project.slug,
|
|
1261
1262
|
jsonOutput: true,
|
|
@@ -1264,18 +1265,18 @@ router.post("/release/:itemId", async (req, res) => {
|
|
|
1264
1265
|
res.status(400).json({ error: result.stderr || "Failed to release item" });
|
|
1265
1266
|
return;
|
|
1266
1267
|
}
|
|
1267
|
-
scheduleGraphSync(req
|
|
1268
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-released");
|
|
1268
1269
|
res.json(result.parsed || { ok: true });
|
|
1269
1270
|
});
|
|
1270
1271
|
// POST /api/projects/:projectId/pm/start-task/:itemId
|
|
1271
1272
|
router.post("/start-task/:itemId", async (req, res) => {
|
|
1272
|
-
const project = await verifyProject(req.user.userId, req
|
|
1273
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1273
1274
|
if (!project) {
|
|
1274
1275
|
res.status(404).json({ error: "Project not found" });
|
|
1275
1276
|
return;
|
|
1276
1277
|
}
|
|
1277
1278
|
const result = runPm({
|
|
1278
|
-
args: ["start-task", req
|
|
1279
|
+
args: ["start-task", routeParam(req, "itemId")],
|
|
1279
1280
|
userId: project.ownerUserId,
|
|
1280
1281
|
slug: project.slug,
|
|
1281
1282
|
jsonOutput: true,
|
|
@@ -1284,18 +1285,18 @@ router.post("/start-task/:itemId", async (req, res) => {
|
|
|
1284
1285
|
res.status(400).json({ error: result.stderr || "Failed to start task" });
|
|
1285
1286
|
return;
|
|
1286
1287
|
}
|
|
1287
|
-
scheduleGraphSync(req
|
|
1288
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-started");
|
|
1288
1289
|
res.json(result.parsed || { ok: true });
|
|
1289
1290
|
});
|
|
1290
1291
|
// POST /api/projects/:projectId/pm/pause-task/:itemId
|
|
1291
1292
|
router.post("/pause-task/:itemId", async (req, res) => {
|
|
1292
|
-
const project = await verifyProject(req.user.userId, req
|
|
1293
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1293
1294
|
if (!project) {
|
|
1294
1295
|
res.status(404).json({ error: "Project not found" });
|
|
1295
1296
|
return;
|
|
1296
1297
|
}
|
|
1297
1298
|
const result = runPm({
|
|
1298
|
-
args: ["pause-task", req
|
|
1299
|
+
args: ["pause-task", routeParam(req, "itemId")],
|
|
1299
1300
|
userId: project.ownerUserId,
|
|
1300
1301
|
slug: project.slug,
|
|
1301
1302
|
jsonOutput: true,
|
|
@@ -1304,18 +1305,18 @@ router.post("/pause-task/:itemId", async (req, res) => {
|
|
|
1304
1305
|
res.status(400).json({ error: result.stderr || "Failed to pause task" });
|
|
1305
1306
|
return;
|
|
1306
1307
|
}
|
|
1307
|
-
scheduleGraphSync(req
|
|
1308
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-paused");
|
|
1308
1309
|
res.json(result.parsed || { ok: true });
|
|
1309
1310
|
});
|
|
1310
1311
|
// GET /api/projects/:projectId/pm/tests/:itemId
|
|
1311
1312
|
router.get("/tests/:itemId", async (req, res) => {
|
|
1312
|
-
const project = await verifyProject(req.user.userId, req
|
|
1313
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1313
1314
|
if (!project) {
|
|
1314
1315
|
res.status(404).json({ error: "Project not found" });
|
|
1315
1316
|
return;
|
|
1316
1317
|
}
|
|
1317
1318
|
const result = runPm({
|
|
1318
|
-
args: ["test", req
|
|
1319
|
+
args: ["test", routeParam(req, "itemId")],
|
|
1319
1320
|
userId: project.ownerUserId,
|
|
1320
1321
|
slug: project.slug,
|
|
1321
1322
|
jsonOutput: true,
|
|
@@ -1324,7 +1325,7 @@ router.get("/tests/:itemId", async (req, res) => {
|
|
|
1324
1325
|
});
|
|
1325
1326
|
// POST /api/projects/:projectId/pm/tests/:itemId
|
|
1326
1327
|
router.post("/tests/:itemId", async (req, res) => {
|
|
1327
|
-
const project = await verifyProject(req.user.userId, req
|
|
1328
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1328
1329
|
if (!project) {
|
|
1329
1330
|
res.status(404).json({ error: "Project not found" });
|
|
1330
1331
|
return;
|
|
@@ -1334,7 +1335,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1334
1335
|
res.status(400).json({ error: "Test command is required" });
|
|
1335
1336
|
return;
|
|
1336
1337
|
}
|
|
1337
|
-
const args = ["test", req
|
|
1338
|
+
const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
|
|
1338
1339
|
if (description)
|
|
1339
1340
|
args.push("--description", description.trim());
|
|
1340
1341
|
const result = runPm({
|
|
@@ -1351,7 +1352,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1351
1352
|
});
|
|
1352
1353
|
// GET /api/projects/:projectId/pm/dedupe-audit
|
|
1353
1354
|
router.get("/dedupe-audit", async (req, res) => {
|
|
1354
|
-
const project = await verifyProject(req.user.userId, req
|
|
1355
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1355
1356
|
if (!project) {
|
|
1356
1357
|
res.status(404).json({ error: "Project not found" });
|
|
1357
1358
|
return;
|
|
@@ -1366,7 +1367,7 @@ router.get("/dedupe-audit", async (req, res) => {
|
|
|
1366
1367
|
});
|
|
1367
1368
|
// GET /api/projects/:projectId/pm/validate
|
|
1368
1369
|
router.get("/validate", async (req, res) => {
|
|
1369
|
-
const project = await verifyProject(req.user.userId, req
|
|
1370
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1370
1371
|
if (!project) {
|
|
1371
1372
|
res.status(404).json({ error: "Project not found" });
|
|
1372
1373
|
return;
|
|
@@ -1381,7 +1382,7 @@ router.get("/validate", async (req, res) => {
|
|
|
1381
1382
|
});
|
|
1382
1383
|
// POST /api/projects/:projectId/pm/restore/:itemId
|
|
1383
1384
|
router.post("/restore/:itemId", async (req, res) => {
|
|
1384
|
-
const project = await verifyProject(req.user.userId, req
|
|
1385
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1385
1386
|
if (!project) {
|
|
1386
1387
|
res.status(404).json({ error: "Project not found" });
|
|
1387
1388
|
return;
|
|
@@ -1392,7 +1393,7 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1392
1393
|
return;
|
|
1393
1394
|
}
|
|
1394
1395
|
const result = runPm({
|
|
1395
|
-
args: ["restore", req
|
|
1396
|
+
args: ["restore", routeParam(req, "itemId"), target.trim()],
|
|
1396
1397
|
userId: project.ownerUserId,
|
|
1397
1398
|
slug: project.slug,
|
|
1398
1399
|
jsonOutput: true,
|
|
@@ -1401,12 +1402,12 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1401
1402
|
res.status(400).json({ error: result.stderr || "Failed to restore item" });
|
|
1402
1403
|
return;
|
|
1403
1404
|
}
|
|
1404
|
-
scheduleGraphSync(req
|
|
1405
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-restored");
|
|
1405
1406
|
res.json(result.parsed || { ok: true });
|
|
1406
1407
|
});
|
|
1407
1408
|
// POST /api/projects/:projectId/pm/close-task/:itemId
|
|
1408
1409
|
router.post("/close-task/:itemId", async (req, res) => {
|
|
1409
|
-
const project = await verifyProject(req.user.userId, req
|
|
1410
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1410
1411
|
if (!project) {
|
|
1411
1412
|
res.status(404).json({ error: "Project not found" });
|
|
1412
1413
|
return;
|
|
@@ -1417,7 +1418,7 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1417
1418
|
return;
|
|
1418
1419
|
}
|
|
1419
1420
|
const result = runPm({
|
|
1420
|
-
args: ["close-task", req
|
|
1421
|
+
args: ["close-task", routeParam(req, "itemId"), reason.trim()],
|
|
1421
1422
|
userId: project.ownerUserId,
|
|
1422
1423
|
slug: project.slug,
|
|
1423
1424
|
jsonOutput: true,
|
|
@@ -1426,12 +1427,12 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1426
1427
|
res.status(400).json({ error: result.stderr || "Failed to close task" });
|
|
1427
1428
|
return;
|
|
1428
1429
|
}
|
|
1429
|
-
scheduleGraphSync(req
|
|
1430
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-closed");
|
|
1430
1431
|
res.json(result.parsed || { ok: true });
|
|
1431
1432
|
});
|
|
1432
1433
|
// POST /api/projects/:projectId/pm/reindex
|
|
1433
1434
|
router.post("/reindex", async (req, res) => {
|
|
1434
|
-
const project = await verifyProject(req.user.userId, req
|
|
1435
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1435
1436
|
if (!project) {
|
|
1436
1437
|
res.status(404).json({ error: "Project not found" });
|
|
1437
1438
|
return;
|
|
@@ -1454,7 +1455,7 @@ router.post("/reindex", async (req, res) => {
|
|
|
1454
1455
|
});
|
|
1455
1456
|
// POST /api/projects/:projectId/pm/normalize
|
|
1456
1457
|
router.post("/normalize", async (req, res) => {
|
|
1457
|
-
const project = await verifyProject(req.user.userId, req
|
|
1458
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1458
1459
|
if (!project) {
|
|
1459
1460
|
res.status(404).json({ error: "Project not found" });
|
|
1460
1461
|
return;
|
|
@@ -1469,7 +1470,7 @@ router.post("/normalize", async (req, res) => {
|
|
|
1469
1470
|
});
|
|
1470
1471
|
// GET /api/projects/:projectId/pm/comments-audit
|
|
1471
1472
|
router.get("/comments-audit", async (req, res) => {
|
|
1472
|
-
const project = await verifyProject(req.user.userId, req
|
|
1473
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1473
1474
|
if (!project) {
|
|
1474
1475
|
res.status(404).json({ error: "Project not found" });
|
|
1475
1476
|
return;
|
|
@@ -1484,7 +1485,7 @@ router.get("/comments-audit", async (req, res) => {
|
|
|
1484
1485
|
});
|
|
1485
1486
|
// POST /api/projects/:projectId/pm/files/:itemId
|
|
1486
1487
|
router.post("/files/:itemId", async (req, res) => {
|
|
1487
|
-
const project = await verifyProject(req.user.userId, req
|
|
1488
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1488
1489
|
if (!project) {
|
|
1489
1490
|
res.status(404).json({ error: "Project not found" });
|
|
1490
1491
|
return;
|
|
@@ -1497,7 +1498,7 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1497
1498
|
let addVal = `path=${filePath.trim()}`;
|
|
1498
1499
|
if (scope)
|
|
1499
1500
|
addVal += `,scope=${scope}`;
|
|
1500
|
-
const args = ["files", req
|
|
1501
|
+
const args = ["files", routeParam(req, "itemId"), "--add", addVal];
|
|
1501
1502
|
const result = runPm({
|
|
1502
1503
|
args,
|
|
1503
1504
|
userId: project.ownerUserId,
|
|
@@ -1508,18 +1509,18 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1508
1509
|
res.status(400).json({ error: result.stderr || "Failed to link file" });
|
|
1509
1510
|
return;
|
|
1510
1511
|
}
|
|
1511
|
-
scheduleGraphSync(req
|
|
1512
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "file-linked");
|
|
1512
1513
|
res.status(201).json(result.parsed || { ok: true });
|
|
1513
1514
|
});
|
|
1514
1515
|
// GET /api/projects/:projectId/pm/files/:itemId
|
|
1515
1516
|
router.get("/files/:itemId", async (req, res) => {
|
|
1516
|
-
const project = await verifyProject(req.user.userId, req
|
|
1517
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1517
1518
|
if (!project) {
|
|
1518
1519
|
res.status(404).json({ error: "Project not found" });
|
|
1519
1520
|
return;
|
|
1520
1521
|
}
|
|
1521
1522
|
const result = runPm({
|
|
1522
|
-
args: ["files", req
|
|
1523
|
+
args: ["files", routeParam(req, "itemId")],
|
|
1523
1524
|
userId: project.ownerUserId,
|
|
1524
1525
|
slug: project.slug,
|
|
1525
1526
|
jsonOutput: true,
|
|
@@ -1528,7 +1529,7 @@ router.get("/files/:itemId", async (req, res) => {
|
|
|
1528
1529
|
});
|
|
1529
1530
|
// GET /api/projects/:projectId/pm/guide — list guide topics
|
|
1530
1531
|
router.get("/guide", async (req, res) => {
|
|
1531
|
-
const project = await verifyProject(req.user.userId, req
|
|
1532
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1532
1533
|
if (!project) {
|
|
1533
1534
|
res.status(404).json({ error: "Project not found" });
|
|
1534
1535
|
return;
|
|
@@ -1543,13 +1544,13 @@ router.get("/guide", async (req, res) => {
|
|
|
1543
1544
|
});
|
|
1544
1545
|
// GET /api/projects/:projectId/pm/guide/:topicId — get single guide topic
|
|
1545
1546
|
router.get("/guide/:topicId", async (req, res) => {
|
|
1546
|
-
const project = await verifyProject(req.user.userId, req
|
|
1547
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1547
1548
|
if (!project) {
|
|
1548
1549
|
res.status(404).json({ error: "Project not found" });
|
|
1549
1550
|
return;
|
|
1550
1551
|
}
|
|
1551
1552
|
const result = runPm({
|
|
1552
|
-
args: ["guide", req
|
|
1553
|
+
args: ["guide", routeParam(req, "topicId")],
|
|
1553
1554
|
userId: project.ownerUserId,
|
|
1554
1555
|
slug: project.slug,
|
|
1555
1556
|
jsonOutput: true,
|
|
@@ -1567,7 +1568,7 @@ router.get("/guide/:topicId", async (req, res) => {
|
|
|
1567
1568
|
// ─────────────────────────────────────────────────────────
|
|
1568
1569
|
// GET /api/projects/:projectId/pm/export?format=json|csv|yaml
|
|
1569
1570
|
router.get("/export", async (req, res) => {
|
|
1570
|
-
const project = await verifyProject(req.user.userId, req
|
|
1571
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1571
1572
|
if (!project) {
|
|
1572
1573
|
res.status(404).json({ error: "Project not found" });
|
|
1573
1574
|
return;
|
|
@@ -1667,7 +1668,7 @@ router.get("/export", async (req, res) => {
|
|
|
1667
1668
|
});
|
|
1668
1669
|
// POST /api/projects/:projectId/pm/import — import JSON items
|
|
1669
1670
|
router.post("/import", async (req, res) => {
|
|
1670
|
-
const project = await verifyProject(req.user.userId, req
|
|
1671
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1671
1672
|
if (!project) {
|
|
1672
1673
|
res.status(404).json({ error: "Project not found" });
|
|
1673
1674
|
return;
|
|
@@ -1723,17 +1724,17 @@ router.post("/import", async (req, res) => {
|
|
|
1723
1724
|
errors.push(`item[${i}]: ${result.stderr || "create failed"}`);
|
|
1724
1725
|
}
|
|
1725
1726
|
}
|
|
1726
|
-
broadcastProjectEvent(req
|
|
1727
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1727
1728
|
type: "items-imported",
|
|
1728
1729
|
data: { count: created.length, userId: req.user.userId },
|
|
1729
1730
|
});
|
|
1730
1731
|
if (created.length > 0)
|
|
1731
|
-
scheduleGraphSync(req
|
|
1732
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-imported");
|
|
1732
1733
|
res.json({ created, errors, total: items.length });
|
|
1733
1734
|
});
|
|
1734
1735
|
// POST /api/projects/:projectId/pm/update-many
|
|
1735
1736
|
router.post("/update-many", async (req, res) => {
|
|
1736
|
-
const project = await verifyProject(req.user.userId, req
|
|
1737
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1737
1738
|
if (!project) {
|
|
1738
1739
|
res.status(404).json({ error: "Project not found" });
|
|
1739
1740
|
return;
|
|
@@ -1776,11 +1777,11 @@ router.post("/update-many", async (req, res) => {
|
|
|
1776
1777
|
res.status(400).json({ error: result.stderr || "update-many failed" });
|
|
1777
1778
|
return;
|
|
1778
1779
|
}
|
|
1779
|
-
broadcastProjectEvent(req
|
|
1780
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1780
1781
|
type: "items-bulk-updated",
|
|
1781
1782
|
data: { userId: req.user.userId },
|
|
1782
1783
|
});
|
|
1783
|
-
scheduleGraphSync(req
|
|
1784
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
|
|
1784
1785
|
res.json(result.parsed || {});
|
|
1785
1786
|
});
|
|
1786
1787
|
// POST /api/projects/:projectId/pm/close-many
|
|
@@ -1788,7 +1789,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
1788
1789
|
// Accepts same filter options as update-many plus a required `reason` field.
|
|
1789
1790
|
// Returns { closed_count, failed_count, skipped_count, rows }.
|
|
1790
1791
|
router.post("/close-many", async (req, res) => {
|
|
1791
|
-
const project = await verifyProject(req.user.userId, req
|
|
1792
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1792
1793
|
if (!project) {
|
|
1793
1794
|
res.status(404).json({ error: "Project not found" });
|
|
1794
1795
|
return;
|
|
@@ -1844,11 +1845,11 @@ router.post("/close-many", async (req, res) => {
|
|
|
1844
1845
|
}
|
|
1845
1846
|
}
|
|
1846
1847
|
if (closedCount > 0) {
|
|
1847
|
-
broadcastProjectEvent(req
|
|
1848
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1848
1849
|
type: "items-bulk-updated",
|
|
1849
1850
|
data: { userId: req.user.userId },
|
|
1850
1851
|
});
|
|
1851
|
-
scheduleGraphSync(req
|
|
1852
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
|
|
1852
1853
|
}
|
|
1853
1854
|
res.json({
|
|
1854
1855
|
closed_count: closedCount,
|
|
@@ -1860,13 +1861,13 @@ router.post("/close-many", async (req, res) => {
|
|
|
1860
1861
|
});
|
|
1861
1862
|
// GET /api/projects/:projectId/pm/docs/:itemId
|
|
1862
1863
|
router.get("/docs/:itemId", async (req, res) => {
|
|
1863
|
-
const project = await verifyProject(req.user.userId, req
|
|
1864
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1864
1865
|
if (!project) {
|
|
1865
1866
|
res.status(404).json({ error: "Project not found" });
|
|
1866
1867
|
return;
|
|
1867
1868
|
}
|
|
1868
1869
|
const result = runPm({
|
|
1869
|
-
args: ["docs", req
|
|
1870
|
+
args: ["docs", routeParam(req, "itemId")],
|
|
1870
1871
|
userId: project.ownerUserId,
|
|
1871
1872
|
slug: project.slug,
|
|
1872
1873
|
jsonOutput: true,
|
|
@@ -1875,13 +1876,13 @@ router.get("/docs/:itemId", async (req, res) => {
|
|
|
1875
1876
|
});
|
|
1876
1877
|
// POST /api/projects/:projectId/pm/docs/:itemId
|
|
1877
1878
|
router.post("/docs/:itemId", async (req, res) => {
|
|
1878
|
-
const project = await verifyProject(req.user.userId, req
|
|
1879
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1879
1880
|
if (!project) {
|
|
1880
1881
|
res.status(404).json({ error: "Project not found" });
|
|
1881
1882
|
return;
|
|
1882
1883
|
}
|
|
1883
1884
|
const { path: docPath, scope, note, remove, validatePaths } = req.body;
|
|
1884
|
-
const args = ["docs", req
|
|
1885
|
+
const args = ["docs", routeParam(req, "itemId")];
|
|
1885
1886
|
if (remove) {
|
|
1886
1887
|
args.push("--remove", remove);
|
|
1887
1888
|
}
|
|
@@ -1905,12 +1906,12 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
1905
1906
|
res.status(400).json({ error: result.stderr || "Failed to update docs" });
|
|
1906
1907
|
return;
|
|
1907
1908
|
}
|
|
1908
|
-
scheduleGraphSync(req
|
|
1909
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "docs-updated");
|
|
1909
1910
|
res.json(result.parsed || { ok: true });
|
|
1910
1911
|
});
|
|
1911
1912
|
// POST /api/projects/:projectId/pm/test-all
|
|
1912
1913
|
router.post("/test-all", async (req, res) => {
|
|
1913
|
-
const project = await verifyProject(req.user.userId, req
|
|
1914
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1914
1915
|
if (!project) {
|
|
1915
1916
|
res.status(404).json({ error: "Project not found" });
|
|
1916
1917
|
return;
|
|
@@ -1932,7 +1933,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
1932
1933
|
});
|
|
1933
1934
|
// GET /api/projects/:projectId/pm/test-runs
|
|
1934
1935
|
router.get("/test-runs", async (req, res) => {
|
|
1935
|
-
const project = await verifyProject(req.user.userId, req
|
|
1936
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1936
1937
|
if (!project) {
|
|
1937
1938
|
res.status(404).json({ error: "Project not found" });
|
|
1938
1939
|
return;
|
|
@@ -1948,7 +1949,7 @@ router.get("/test-runs", async (req, res) => {
|
|
|
1948
1949
|
});
|
|
1949
1950
|
// POST /api/projects/:projectId/pm/gc
|
|
1950
1951
|
router.post("/gc", async (req, res) => {
|
|
1951
|
-
const project = await verifyProject(req.user.userId, req
|
|
1952
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1952
1953
|
if (!project) {
|
|
1953
1954
|
res.status(404).json({ error: "Project not found" });
|
|
1954
1955
|
return;
|
|
@@ -1958,7 +1959,7 @@ router.post("/gc", async (req, res) => {
|
|
|
1958
1959
|
});
|
|
1959
1960
|
// GET /api/projects/:projectId/pm/templates
|
|
1960
1961
|
router.get("/templates", async (req, res) => {
|
|
1961
|
-
const project = await verifyProject(req.user.userId, req
|
|
1962
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1962
1963
|
if (!project) {
|
|
1963
1964
|
res.status(404).json({ error: "Project not found" });
|
|
1964
1965
|
return;
|
|
@@ -1968,17 +1969,17 @@ router.get("/templates", async (req, res) => {
|
|
|
1968
1969
|
});
|
|
1969
1970
|
// GET /api/projects/:projectId/pm/templates/:name
|
|
1970
1971
|
router.get("/templates/:name", async (req, res) => {
|
|
1971
|
-
const project = await verifyProject(req.user.userId, req
|
|
1972
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1972
1973
|
if (!project) {
|
|
1973
1974
|
res.status(404).json({ error: "Project not found" });
|
|
1974
1975
|
return;
|
|
1975
1976
|
}
|
|
1976
|
-
const result = runPm({ args: ["templates", "show", req
|
|
1977
|
+
const result = runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1977
1978
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1978
1979
|
});
|
|
1979
1980
|
// GET /api/projects/:projectId/pm/config
|
|
1980
1981
|
router.get("/config", async (req, res) => {
|
|
1981
|
-
const project = await verifyProject(req.user.userId, req
|
|
1982
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1982
1983
|
if (!project) {
|
|
1983
1984
|
res.status(404).json({ error: "Project not found" });
|
|
1984
1985
|
return;
|
|
@@ -1988,23 +1989,23 @@ router.get("/config", async (req, res) => {
|
|
|
1988
1989
|
});
|
|
1989
1990
|
// GET /api/projects/:projectId/pm/config/:key
|
|
1990
1991
|
router.get("/config/:key", async (req, res) => {
|
|
1991
|
-
const project = await verifyProject(req.user.userId, req
|
|
1992
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1992
1993
|
if (!project) {
|
|
1993
1994
|
res.status(404).json({ error: "Project not found" });
|
|
1994
1995
|
return;
|
|
1995
1996
|
}
|
|
1996
|
-
const key = req
|
|
1997
|
+
const key = routeParam(req, "key");
|
|
1997
1998
|
const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1998
1999
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1999
2000
|
});
|
|
2000
2001
|
// PATCH /api/projects/:projectId/pm/config/:key
|
|
2001
2002
|
router.patch("/config/:key", async (req, res) => {
|
|
2002
|
-
const project = await verifyProject(req.user.userId, req
|
|
2003
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2003
2004
|
if (!project) {
|
|
2004
2005
|
res.status(404).json({ error: "Project not found" });
|
|
2005
2006
|
return;
|
|
2006
2007
|
}
|
|
2007
|
-
const key = req
|
|
2008
|
+
const key = routeParam(req, "key");
|
|
2008
2009
|
const body = req.body;
|
|
2009
2010
|
const args = ["config", "project", "set", key];
|
|
2010
2011
|
if (body.value)
|
|
@@ -2020,7 +2021,7 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
2020
2021
|
// These wrap pm list-draft, list-open, etc.
|
|
2021
2022
|
function buildListShortcutRoute(pmCommand) {
|
|
2022
2023
|
return async (req, res) => {
|
|
2023
|
-
const project = await verifyProject(req.user.userId, req
|
|
2024
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2024
2025
|
if (!project) {
|
|
2025
2026
|
res.status(404).json({ error: "Project not found" });
|
|
2026
2027
|
return;
|
|
@@ -2058,7 +2059,7 @@ router.get("/list-canceled", buildListShortcutRoute("list-canceled"));
|
|
|
2058
2059
|
// ─────────────────────────────────────────────────────────
|
|
2059
2060
|
// POST /api/projects/:projectId/pm/plan
|
|
2060
2061
|
router.post("/plan", async (req, res) => {
|
|
2061
|
-
const project = await verifyProject(req.user.userId, req
|
|
2062
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2062
2063
|
if (!project) {
|
|
2063
2064
|
res.status(404).json({ error: "Project not found" });
|
|
2064
2065
|
return;
|
|
@@ -2084,7 +2085,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2084
2085
|
res.status(400).json({ error: result.stderr || "Failed to create plan" });
|
|
2085
2086
|
return;
|
|
2086
2087
|
}
|
|
2087
|
-
broadcastProjectEvent(req
|
|
2088
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2088
2089
|
type: "item-created",
|
|
2089
2090
|
data: { result: result.parsed, userId: req.user.userId },
|
|
2090
2091
|
});
|
|
@@ -2092,13 +2093,13 @@ router.post("/plan", async (req, res) => {
|
|
|
2092
2093
|
});
|
|
2093
2094
|
// GET /api/projects/:projectId/pm/plan/:planId
|
|
2094
2095
|
router.get("/plan/:planId", async (req, res) => {
|
|
2095
|
-
const project = await verifyProject(req.user.userId, req
|
|
2096
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2096
2097
|
if (!project) {
|
|
2097
2098
|
res.status(404).json({ error: "Project not found" });
|
|
2098
2099
|
return;
|
|
2099
2100
|
}
|
|
2100
2101
|
const result = runPm({
|
|
2101
|
-
args: ["plan", "show", req
|
|
2102
|
+
args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
|
|
2102
2103
|
userId: project.ownerUserId,
|
|
2103
2104
|
slug: project.slug,
|
|
2104
2105
|
jsonOutput: true,
|
|
@@ -2111,13 +2112,13 @@ router.get("/plan/:planId", async (req, res) => {
|
|
|
2111
2112
|
});
|
|
2112
2113
|
// PATCH /api/projects/:projectId/pm/plan/:planId
|
|
2113
2114
|
router.patch("/plan/:planId", async (req, res) => {
|
|
2114
|
-
const project = await verifyProject(req.user.userId, req
|
|
2115
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2115
2116
|
if (!project) {
|
|
2116
2117
|
res.status(404).json({ error: "Project not found" });
|
|
2117
2118
|
return;
|
|
2118
2119
|
}
|
|
2119
2120
|
const { title, description } = req.body;
|
|
2120
|
-
const args = ["update", req
|
|
2121
|
+
const args = ["update", routeParam(req, "planId")];
|
|
2121
2122
|
if (title?.trim())
|
|
2122
2123
|
args.push("--title", title.trim());
|
|
2123
2124
|
if (description !== undefined)
|
|
@@ -2127,21 +2128,21 @@ router.patch("/plan/:planId", async (req, res) => {
|
|
|
2127
2128
|
res.status(400).json({ error: result.stderr || "Failed to update plan" });
|
|
2128
2129
|
return;
|
|
2129
2130
|
}
|
|
2130
|
-
broadcastProjectEvent(req
|
|
2131
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2131
2132
|
type: "item-updated",
|
|
2132
|
-
data: { itemId: req
|
|
2133
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2133
2134
|
});
|
|
2134
2135
|
res.json(result.parsed || {});
|
|
2135
2136
|
});
|
|
2136
2137
|
// DELETE /api/projects/:projectId/pm/plan/:planId
|
|
2137
2138
|
router.delete("/plan/:planId", async (req, res) => {
|
|
2138
|
-
const project = await verifyProject(req.user.userId, req
|
|
2139
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2139
2140
|
if (!project) {
|
|
2140
2141
|
res.status(404).json({ error: "Project not found" });
|
|
2141
2142
|
return;
|
|
2142
2143
|
}
|
|
2143
2144
|
const result = runPm({
|
|
2144
|
-
args: ["delete", req
|
|
2145
|
+
args: ["delete", routeParam(req, "planId")],
|
|
2145
2146
|
userId: project.ownerUserId,
|
|
2146
2147
|
slug: project.slug,
|
|
2147
2148
|
jsonOutput: true,
|
|
@@ -2150,15 +2151,15 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2150
2151
|
res.status(400).json({ error: result.stderr || "Failed to delete plan" });
|
|
2151
2152
|
return;
|
|
2152
2153
|
}
|
|
2153
|
-
broadcastProjectEvent(req
|
|
2154
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2154
2155
|
type: "item-deleted",
|
|
2155
|
-
data: { itemId: req
|
|
2156
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2156
2157
|
});
|
|
2157
2158
|
res.json({ ok: true });
|
|
2158
2159
|
});
|
|
2159
2160
|
// POST /api/projects/:projectId/pm/plan/:planId/steps
|
|
2160
2161
|
router.post("/plan/:planId/steps", async (req, res) => {
|
|
2161
|
-
const project = await verifyProject(req.user.userId, req
|
|
2162
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2162
2163
|
if (!project) {
|
|
2163
2164
|
res.status(404).json({ error: "Project not found" });
|
|
2164
2165
|
return;
|
|
@@ -2168,7 +2169,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2168
2169
|
res.status(400).json({ error: "Title is required" });
|
|
2169
2170
|
return;
|
|
2170
2171
|
}
|
|
2171
|
-
const args = ["plan", "add-step", req
|
|
2172
|
+
const args = ["plan", "add-step", routeParam(req, "planId"), "--title", title.trim()];
|
|
2172
2173
|
if (description)
|
|
2173
2174
|
args.push("--description", description);
|
|
2174
2175
|
if (dependsOn)
|
|
@@ -2178,21 +2179,21 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2178
2179
|
res.status(400).json({ error: result.stderr || "Failed to add step" });
|
|
2179
2180
|
return;
|
|
2180
2181
|
}
|
|
2181
|
-
broadcastProjectEvent(req
|
|
2182
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2182
2183
|
type: "item-updated",
|
|
2183
|
-
data: { itemId: req
|
|
2184
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2184
2185
|
});
|
|
2185
2186
|
res.status(201).json(result.parsed || {});
|
|
2186
2187
|
});
|
|
2187
2188
|
// PATCH /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2188
2189
|
router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2189
|
-
const project = await verifyProject(req.user.userId, req
|
|
2190
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2190
2191
|
if (!project) {
|
|
2191
2192
|
res.status(404).json({ error: "Project not found" });
|
|
2192
2193
|
return;
|
|
2193
2194
|
}
|
|
2194
2195
|
const { title, description } = req.body;
|
|
2195
|
-
const args = ["plan", "update-step", req
|
|
2196
|
+
const args = ["plan", "update-step", routeParam(req, "planId"), routeParam(req, "stepRef")];
|
|
2196
2197
|
if (title)
|
|
2197
2198
|
args.push("--title", title);
|
|
2198
2199
|
if (description)
|
|
@@ -2202,21 +2203,21 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2202
2203
|
res.status(400).json({ error: result.stderr || "Failed to update step" });
|
|
2203
2204
|
return;
|
|
2204
2205
|
}
|
|
2205
|
-
broadcastProjectEvent(req
|
|
2206
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2206
2207
|
type: "item-updated",
|
|
2207
|
-
data: { itemId: req
|
|
2208
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2208
2209
|
});
|
|
2209
2210
|
res.json(result.parsed || {});
|
|
2210
2211
|
});
|
|
2211
2212
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/complete
|
|
2212
2213
|
router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
2213
|
-
const project = await verifyProject(req.user.userId, req
|
|
2214
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2214
2215
|
if (!project) {
|
|
2215
2216
|
res.status(404).json({ error: "Project not found" });
|
|
2216
2217
|
return;
|
|
2217
2218
|
}
|
|
2218
2219
|
const result = runPm({
|
|
2219
|
-
args: ["plan", "complete-step", req
|
|
2220
|
+
args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2220
2221
|
userId: project.ownerUserId,
|
|
2221
2222
|
slug: project.slug,
|
|
2222
2223
|
jsonOutput: true,
|
|
@@ -2225,15 +2226,15 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
|
2225
2226
|
res.status(400).json({ error: result.stderr || "Failed to complete step" });
|
|
2226
2227
|
return;
|
|
2227
2228
|
}
|
|
2228
|
-
broadcastProjectEvent(req
|
|
2229
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2229
2230
|
type: "item-updated",
|
|
2230
|
-
data: { itemId: req
|
|
2231
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2231
2232
|
});
|
|
2232
2233
|
res.json(result.parsed || {});
|
|
2233
2234
|
});
|
|
2234
2235
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/block
|
|
2235
2236
|
router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
2236
|
-
const project = await verifyProject(req.user.userId, req
|
|
2237
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2237
2238
|
if (!project) {
|
|
2238
2239
|
res.status(404).json({ error: "Project not found" });
|
|
2239
2240
|
return;
|
|
@@ -2244,7 +2245,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2244
2245
|
return;
|
|
2245
2246
|
}
|
|
2246
2247
|
const result = runPm({
|
|
2247
|
-
args: ["plan", "block-step", req
|
|
2248
|
+
args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
|
|
2248
2249
|
userId: project.ownerUserId,
|
|
2249
2250
|
slug: project.slug,
|
|
2250
2251
|
jsonOutput: true,
|
|
@@ -2253,21 +2254,21 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2253
2254
|
res.status(400).json({ error: result.stderr || "Failed to block step" });
|
|
2254
2255
|
return;
|
|
2255
2256
|
}
|
|
2256
|
-
broadcastProjectEvent(req
|
|
2257
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2257
2258
|
type: "item-updated",
|
|
2258
|
-
data: { itemId: req
|
|
2259
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2259
2260
|
});
|
|
2260
2261
|
res.json(result.parsed || {});
|
|
2261
2262
|
});
|
|
2262
2263
|
// DELETE /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2263
2264
|
router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2264
|
-
const project = await verifyProject(req.user.userId, req
|
|
2265
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2265
2266
|
if (!project) {
|
|
2266
2267
|
res.status(404).json({ error: "Project not found" });
|
|
2267
2268
|
return;
|
|
2268
2269
|
}
|
|
2269
2270
|
const result = runPm({
|
|
2270
|
-
args: ["plan", "remove-step", req
|
|
2271
|
+
args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2271
2272
|
userId: project.ownerUserId,
|
|
2272
2273
|
slug: project.slug,
|
|
2273
2274
|
jsonOutput: true,
|
|
@@ -2276,21 +2277,21 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2276
2277
|
res.status(400).json({ error: result.stderr || "Failed to remove step" });
|
|
2277
2278
|
return;
|
|
2278
2279
|
}
|
|
2279
|
-
broadcastProjectEvent(req
|
|
2280
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2280
2281
|
type: "item-updated",
|
|
2281
|
-
data: { itemId: req
|
|
2282
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2282
2283
|
});
|
|
2283
2284
|
res.json(result.parsed || {});
|
|
2284
2285
|
});
|
|
2285
2286
|
// POST /api/projects/:projectId/pm/plan/:planId/approve
|
|
2286
2287
|
router.post("/plan/:planId/approve", async (req, res) => {
|
|
2287
|
-
const project = await verifyProject(req.user.userId, req
|
|
2288
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2288
2289
|
if (!project) {
|
|
2289
2290
|
res.status(404).json({ error: "Project not found" });
|
|
2290
2291
|
return;
|
|
2291
2292
|
}
|
|
2292
2293
|
const result = runPm({
|
|
2293
|
-
args: ["plan", "approve", req
|
|
2294
|
+
args: ["plan", "approve", routeParam(req, "planId")],
|
|
2294
2295
|
userId: project.ownerUserId,
|
|
2295
2296
|
slug: project.slug,
|
|
2296
2297
|
jsonOutput: true,
|
|
@@ -2299,21 +2300,21 @@ router.post("/plan/:planId/approve", async (req, res) => {
|
|
|
2299
2300
|
res.status(400).json({ error: result.stderr || "Failed to approve plan" });
|
|
2300
2301
|
return;
|
|
2301
2302
|
}
|
|
2302
|
-
broadcastProjectEvent(req
|
|
2303
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2303
2304
|
type: "item-updated",
|
|
2304
|
-
data: { itemId: req
|
|
2305
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2305
2306
|
});
|
|
2306
2307
|
res.json(result.parsed || {});
|
|
2307
2308
|
});
|
|
2308
2309
|
// POST /api/projects/:projectId/pm/plan/:planId/materialize
|
|
2309
2310
|
router.post("/plan/:planId/materialize", async (req, res) => {
|
|
2310
|
-
const project = await verifyProject(req.user.userId, req
|
|
2311
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2311
2312
|
if (!project) {
|
|
2312
2313
|
res.status(404).json({ error: "Project not found" });
|
|
2313
2314
|
return;
|
|
2314
2315
|
}
|
|
2315
2316
|
const { materializeType, materializeParent, steps } = req.body;
|
|
2316
|
-
const args = ["plan", "materialize", req
|
|
2317
|
+
const args = ["plan", "materialize", routeParam(req, "planId")];
|
|
2317
2318
|
if (materializeType)
|
|
2318
2319
|
args.push("--materialize-type", materializeType);
|
|
2319
2320
|
if (materializeParent)
|
|
@@ -2325,7 +2326,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2325
2326
|
res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2326
2327
|
return;
|
|
2327
2328
|
}
|
|
2328
|
-
broadcastProjectEvent(req
|
|
2329
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2329
2330
|
type: "item-created",
|
|
2330
2331
|
data: { result: result.parsed, userId: req.user.userId },
|
|
2331
2332
|
});
|
|
@@ -2333,7 +2334,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2333
2334
|
});
|
|
2334
2335
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/reorder
|
|
2335
2336
|
router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
2336
|
-
const project = await verifyProject(req.user.userId, req
|
|
2337
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2337
2338
|
if (!project) {
|
|
2338
2339
|
res.status(404).json({ error: "Project not found" });
|
|
2339
2340
|
return;
|
|
@@ -2344,7 +2345,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2344
2345
|
return;
|
|
2345
2346
|
}
|
|
2346
2347
|
const result = runPm({
|
|
2347
|
-
args: ["plan", "reorder-step", req
|
|
2348
|
+
args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
|
|
2348
2349
|
userId: project.ownerUserId,
|
|
2349
2350
|
slug: project.slug,
|
|
2350
2351
|
jsonOutput: true,
|
|
@@ -2353,15 +2354,15 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2353
2354
|
res.status(400).json({ error: result.stderr || "Failed to reorder step" });
|
|
2354
2355
|
return;
|
|
2355
2356
|
}
|
|
2356
|
-
broadcastProjectEvent(req
|
|
2357
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2357
2358
|
type: "item-updated",
|
|
2358
|
-
data: { itemId: req
|
|
2359
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2359
2360
|
});
|
|
2360
2361
|
res.json(result.parsed || {});
|
|
2361
2362
|
});
|
|
2362
2363
|
// POST /api/projects/:projectId/pm/plan/:planId/link
|
|
2363
2364
|
router.post("/plan/:planId/link", async (req, res) => {
|
|
2364
|
-
const project = await verifyProject(req.user.userId, req
|
|
2365
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2365
2366
|
if (!project) {
|
|
2366
2367
|
res.status(404).json({ error: "Project not found" });
|
|
2367
2368
|
return;
|
|
@@ -2371,7 +2372,7 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2371
2372
|
res.status(400).json({ error: "link (item id) is required" });
|
|
2372
2373
|
return;
|
|
2373
2374
|
}
|
|
2374
|
-
const args = ["plan", "link", req
|
|
2375
|
+
const args = ["plan", "link", routeParam(req, "planId"), "--link", link.trim()];
|
|
2375
2376
|
if (linkKind)
|
|
2376
2377
|
args.push("--link-kind", linkKind);
|
|
2377
2378
|
if (linkNote)
|
|
@@ -2383,15 +2384,15 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2383
2384
|
res.status(400).json({ error: result.stderr || "Failed to link plan" });
|
|
2384
2385
|
return;
|
|
2385
2386
|
}
|
|
2386
|
-
broadcastProjectEvent(req
|
|
2387
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2387
2388
|
type: "item-updated",
|
|
2388
|
-
data: { itemId: req
|
|
2389
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2389
2390
|
});
|
|
2390
2391
|
res.status(201).json(result.parsed || {});
|
|
2391
2392
|
});
|
|
2392
2393
|
// DELETE /api/projects/:projectId/pm/plan/:planId/link
|
|
2393
2394
|
router.delete("/plan/:planId/link", async (req, res) => {
|
|
2394
|
-
const project = await verifyProject(req.user.userId, req
|
|
2395
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2395
2396
|
if (!project) {
|
|
2396
2397
|
res.status(404).json({ error: "Project not found" });
|
|
2397
2398
|
return;
|
|
@@ -2401,7 +2402,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2401
2402
|
res.status(400).json({ error: "link (item id) is required" });
|
|
2402
2403
|
return;
|
|
2403
2404
|
}
|
|
2404
|
-
const args = ["plan", "unlink", req
|
|
2405
|
+
const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
|
|
2405
2406
|
if (linkKind)
|
|
2406
2407
|
args.push("--link-kind", linkKind);
|
|
2407
2408
|
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
@@ -2409,9 +2410,9 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2409
2410
|
res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2410
2411
|
return;
|
|
2411
2412
|
}
|
|
2412
|
-
broadcastProjectEvent(req
|
|
2413
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2413
2414
|
type: "item-updated",
|
|
2414
|
-
data: { itemId: req
|
|
2415
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2415
2416
|
});
|
|
2416
2417
|
res.json(result.parsed || {});
|
|
2417
2418
|
});
|
|
@@ -2420,7 +2421,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2420
2421
|
// POST /api/projects/:projectId/pm/upgrade
|
|
2421
2422
|
// Runs pm upgrade --packages-only (never upgrades the CLI itself via the web UI).
|
|
2422
2423
|
router.get("/upgrade", async (req, res) => {
|
|
2423
|
-
const project = await verifyProject(req.user.userId, req
|
|
2424
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2424
2425
|
if (!project) {
|
|
2425
2426
|
res.status(404).json({ error: "Project not found" });
|
|
2426
2427
|
return;
|
|
@@ -2434,7 +2435,7 @@ router.get("/upgrade", async (req, res) => {
|
|
|
2434
2435
|
res.json(result.ok ? (result.parsed || { dryRun: true }) : { error: result.stderr });
|
|
2435
2436
|
});
|
|
2436
2437
|
router.post("/upgrade", async (req, res) => {
|
|
2437
|
-
const project = await verifyProject(req.user.userId, req
|
|
2438
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2438
2439
|
if (!project) {
|
|
2439
2440
|
res.status(404).json({ error: "Project not found" });
|
|
2440
2441
|
return;
|
|
@@ -2455,7 +2456,7 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2455
2456
|
// ─── Presence endpoints ───
|
|
2456
2457
|
// GET /api/projects/:projectId/pm/presence
|
|
2457
2458
|
router.get("/presence", async (req, res) => {
|
|
2458
|
-
const projectId = req
|
|
2459
|
+
const projectId = routeParam(req, "projectId");
|
|
2459
2460
|
res.json({ users: getProjectPresence(projectId) });
|
|
2460
2461
|
});
|
|
2461
2462
|
// PATCH /api/projects/:projectId/pm/presence/:clientId
|
|
@@ -2470,7 +2471,7 @@ router.patch("/presence/:clientId", async (req, res) => {
|
|
|
2470
2471
|
// GET /api/projects/:projectId/pm/events
|
|
2471
2472
|
router.get("/events", async (req, res) => {
|
|
2472
2473
|
try {
|
|
2473
|
-
const project = await verifyProject(req.user.userId, req
|
|
2474
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2474
2475
|
if (!project) {
|
|
2475
2476
|
res.status(404).json({ error: "Project not found" });
|
|
2476
2477
|
return;
|
|
@@ -2483,7 +2484,7 @@ router.get("/events", async (req, res) => {
|
|
|
2483
2484
|
}
|
|
2484
2485
|
setupSSEHeaders(res);
|
|
2485
2486
|
const clientId = uuidv4();
|
|
2486
|
-
const projectId = req
|
|
2487
|
+
const projectId = routeParam(req, "projectId");
|
|
2487
2488
|
const userId = req.user.userId;
|
|
2488
2489
|
// Client sends display name as query param; fall back to email
|
|
2489
2490
|
const displayName = String(req.query["dn"] ?? req.user.email ?? userId);
|
|
@@ -2517,12 +2518,12 @@ router.get("/events", async (req, res) => {
|
|
|
2517
2518
|
// ─── Presence endpoint ───
|
|
2518
2519
|
// GET /api/projects/:projectId/pm/presence
|
|
2519
2520
|
router.get("/presence", async (req, res) => {
|
|
2520
|
-
const project = await verifyProject(req.user.userId, req
|
|
2521
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2521
2522
|
if (!project) {
|
|
2522
2523
|
res.status(404).json({ error: "Project not found" });
|
|
2523
2524
|
return;
|
|
2524
2525
|
}
|
|
2525
|
-
const users = getProjectPresence(req
|
|
2526
|
+
const users = getProjectPresence(routeParam(req, "projectId"));
|
|
2526
2527
|
res.json({ users });
|
|
2527
2528
|
});
|
|
2528
2529
|
export { router as pmRouter };
|