@unbrained/pm-web 2026.6.2 → 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 +36 -0
- package/README.md +92 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/board.js +27 -7
- package/dist/board.js.map +1 -1
- package/dist/ical.js +141 -0
- package/dist/ical.js.map +1 -0
- package/dist/index.js +351 -6
- package/dist/index.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 +241 -197
- 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/dist/server.js +13 -2
- package/dist/server.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +13 -13
- package/public/index.html +2 -1
- package/public/src/app.js +12 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +12 -1
- package/public/src/filters.js +40 -0
- package/public/src/filters.js.map +1 -0
- package/public/src/filters.ts +52 -0
- package/public/src/state.js +1 -1
- package/public/src/state.js.map +1 -1
- package/public/src/state.ts +1 -1
- package/public/src/theme.js +74 -0
- package/public/src/theme.js.map +1 -0
- package/public/src/theme.ts +75 -0
- package/public/src/types.ts +1 -0
- package/public/src/utils.js +3 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +3 -1
- package/public/src/views/calendar.js +1 -0
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +1 -0
- package/public/src/views/items.js +56 -3
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +55 -3
- package/public/styles.css +85 -0
package/dist/routes/pm.js
CHANGED
|
@@ -2,10 +2,12 @@ import { Router } from "express";
|
|
|
2
2
|
import { requireAuth } from "../middleware/auth.js";
|
|
3
3
|
import { ensureGraphExtension, runPm } from "../services/pm-runner.js";
|
|
4
4
|
import { boardColumns, filterItemsByQuery } from "../board.js";
|
|
5
|
+
import { buildIcsCalendar } from "../ical.js";
|
|
5
6
|
import { verifyProjectAccess } from "./projects.js";
|
|
6
7
|
import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, broadcastPresence, updateClientView, getProjectPresence } from "../services/sse.js";
|
|
7
8
|
import { v4 as uuidv4 } from "uuid";
|
|
8
9
|
import neo4j from "neo4j-driver";
|
|
10
|
+
import { routeParam } from "./route-params.js";
|
|
9
11
|
// Singleton Neo4j driver — reused across sync calls to avoid per-call connection overhead.
|
|
10
12
|
let _neo4jDriver = null;
|
|
11
13
|
let _neo4jDriverKey = "";
|
|
@@ -31,7 +33,7 @@ router.use(async (req, res, next) => {
|
|
|
31
33
|
next();
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
const projectId = req
|
|
36
|
+
const projectId = routeParam(req, "projectId");
|
|
35
37
|
if (!projectId) {
|
|
36
38
|
next();
|
|
37
39
|
return;
|
|
@@ -334,7 +336,7 @@ async function verifyProject(userId, projectId) {
|
|
|
334
336
|
// Returns runtime types/statuses from `pm contracts --json` so the frontend
|
|
335
337
|
// stays in sync with whatever pm CLI version + extensions are installed.
|
|
336
338
|
router.get("/schema", async (req, res) => {
|
|
337
|
-
const project = await verifyProject(req.user.userId, req
|
|
339
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
338
340
|
if (!project) {
|
|
339
341
|
res.status(404).json({ error: "Project not found" });
|
|
340
342
|
return;
|
|
@@ -352,7 +354,7 @@ router.get("/schema", async (req, res) => {
|
|
|
352
354
|
});
|
|
353
355
|
// GET /api/projects/:projectId/pm/list
|
|
354
356
|
router.get("/list", async (req, res) => {
|
|
355
|
-
const project = await verifyProject(req.user.userId, req
|
|
357
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
356
358
|
if (!project) {
|
|
357
359
|
res.status(404).json({ error: "Project not found" });
|
|
358
360
|
return;
|
|
@@ -378,7 +380,7 @@ router.get("/list", async (req, res) => {
|
|
|
378
380
|
});
|
|
379
381
|
// GET /api/projects/:projectId/pm/list-all
|
|
380
382
|
router.get("/list-all", async (req, res) => {
|
|
381
|
-
const project = await verifyProject(req.user.userId, req
|
|
383
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
382
384
|
if (!project) {
|
|
383
385
|
res.status(404).json({ error: "Project not found" });
|
|
384
386
|
return;
|
|
@@ -396,7 +398,7 @@ router.get("/list-all", async (req, res) => {
|
|
|
396
398
|
// Kanban board: items grouped into columns by the workspace's runtime statuses
|
|
397
399
|
// (read live from `pm contracts`) so the board matches the installed CLI.
|
|
398
400
|
router.get("/board", async (req, res) => {
|
|
399
|
-
const project = await verifyProject(req.user.userId, req
|
|
401
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
400
402
|
if (!project) {
|
|
401
403
|
res.status(404).json({ error: "Project not found" });
|
|
402
404
|
return;
|
|
@@ -417,7 +419,7 @@ router.get("/board", async (req, res) => {
|
|
|
417
419
|
// GET /api/projects/:projectId/pm/search?q=...
|
|
418
420
|
// Full-text search over id/title/tags/body via a single list-all read.
|
|
419
421
|
router.get("/search", async (req, res) => {
|
|
420
|
-
const project = await verifyProject(req.user.userId, req
|
|
422
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
421
423
|
if (!project) {
|
|
422
424
|
res.status(404).json({ error: "Project not found" });
|
|
423
425
|
return;
|
|
@@ -433,7 +435,7 @@ router.get("/search", async (req, res) => {
|
|
|
433
435
|
});
|
|
434
436
|
// POST /api/projects/:projectId/pm/create
|
|
435
437
|
router.post("/create", async (req, res) => {
|
|
436
|
-
const project = await verifyProject(req.user.userId, req
|
|
438
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
437
439
|
if (!project) {
|
|
438
440
|
res.status(404).json({ error: "Project not found" });
|
|
439
441
|
return;
|
|
@@ -512,22 +514,22 @@ router.post("/create", async (req, res) => {
|
|
|
512
514
|
return;
|
|
513
515
|
}
|
|
514
516
|
// Broadcast SSE create event
|
|
515
|
-
broadcastProjectEvent(req
|
|
517
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
516
518
|
type: "item-created",
|
|
517
519
|
data: { result: result.parsed, userId: req.user.userId },
|
|
518
520
|
});
|
|
519
|
-
scheduleGraphSync(req
|
|
521
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-created");
|
|
520
522
|
res.status(201).json(result.parsed || {});
|
|
521
523
|
});
|
|
522
524
|
// GET /api/projects/:projectId/pm/get/:itemId
|
|
523
525
|
router.get("/get/:itemId", async (req, res) => {
|
|
524
|
-
const project = await verifyProject(req.user.userId, req
|
|
526
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
525
527
|
if (!project) {
|
|
526
528
|
res.status(404).json({ error: "Project not found" });
|
|
527
529
|
return;
|
|
528
530
|
}
|
|
529
531
|
const result = runPm({
|
|
530
|
-
args: ["get", req
|
|
532
|
+
args: ["get", routeParam(req, "itemId")],
|
|
531
533
|
userId: project.ownerUserId,
|
|
532
534
|
slug: project.slug,
|
|
533
535
|
jsonOutput: true,
|
|
@@ -540,13 +542,13 @@ router.get("/get/:itemId", async (req, res) => {
|
|
|
540
542
|
});
|
|
541
543
|
// PATCH /api/projects/:projectId/pm/update/:itemId
|
|
542
544
|
router.patch("/update/:itemId", async (req, res) => {
|
|
543
|
-
const project = await verifyProject(req.user.userId, req
|
|
545
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
544
546
|
if (!project) {
|
|
545
547
|
res.status(404).json({ error: "Project not found" });
|
|
546
548
|
return;
|
|
547
549
|
}
|
|
548
550
|
const body = req.body;
|
|
549
|
-
const args = ["update", req
|
|
551
|
+
const args = ["update", routeParam(req, "itemId")];
|
|
550
552
|
// String options
|
|
551
553
|
const stringFlags = {
|
|
552
554
|
title: "--title", description: "--description", status: "--status", priority: "--priority",
|
|
@@ -579,16 +581,16 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
579
581
|
return;
|
|
580
582
|
}
|
|
581
583
|
// Broadcast SSE update event
|
|
582
|
-
broadcastProjectEvent(req
|
|
584
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
583
585
|
type: "item-updated",
|
|
584
|
-
data: { itemId: req
|
|
586
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
585
587
|
});
|
|
586
|
-
scheduleGraphSync(req
|
|
588
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-updated");
|
|
587
589
|
res.json(result.parsed || {});
|
|
588
590
|
});
|
|
589
591
|
// POST /api/projects/:projectId/pm/close/:itemId
|
|
590
592
|
router.post("/close/:itemId", async (req, res) => {
|
|
591
|
-
const project = await verifyProject(req.user.userId, req
|
|
593
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
592
594
|
if (!project) {
|
|
593
595
|
res.status(404).json({ error: "Project not found" });
|
|
594
596
|
return;
|
|
@@ -599,7 +601,7 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
599
601
|
return;
|
|
600
602
|
}
|
|
601
603
|
const result = runPm({
|
|
602
|
-
args: ["close", req
|
|
604
|
+
args: ["close", routeParam(req, "itemId"), reason.trim()],
|
|
603
605
|
userId: project.ownerUserId,
|
|
604
606
|
slug: project.slug,
|
|
605
607
|
jsonOutput: true,
|
|
@@ -608,22 +610,22 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
608
610
|
res.status(400).json({ error: result.stderr || "Failed to close item" });
|
|
609
611
|
return;
|
|
610
612
|
}
|
|
611
|
-
broadcastProjectEvent(req
|
|
613
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
612
614
|
type: "item-closed",
|
|
613
|
-
data: { itemId: req
|
|
615
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
614
616
|
});
|
|
615
|
-
scheduleGraphSync(req
|
|
617
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-closed");
|
|
616
618
|
res.json(result.parsed || {});
|
|
617
619
|
});
|
|
618
620
|
// DELETE /api/projects/:projectId/pm/delete/:itemId
|
|
619
621
|
router.delete("/delete/:itemId", async (req, res) => {
|
|
620
|
-
const project = await verifyProject(req.user.userId, req
|
|
622
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
621
623
|
if (!project) {
|
|
622
624
|
res.status(404).json({ error: "Project not found" });
|
|
623
625
|
return;
|
|
624
626
|
}
|
|
625
627
|
const result = runPm({
|
|
626
|
-
args: ["delete", req
|
|
628
|
+
args: ["delete", routeParam(req, "itemId"), "--yes"],
|
|
627
629
|
userId: project.ownerUserId,
|
|
628
630
|
slug: project.slug,
|
|
629
631
|
});
|
|
@@ -631,16 +633,16 @@ router.delete("/delete/:itemId", async (req, res) => {
|
|
|
631
633
|
res.status(400).json({ error: result.stderr || "Failed to delete item" });
|
|
632
634
|
return;
|
|
633
635
|
}
|
|
634
|
-
broadcastProjectEvent(req
|
|
636
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
635
637
|
type: "item-deleted",
|
|
636
|
-
data: { itemId: req
|
|
638
|
+
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
637
639
|
});
|
|
638
|
-
scheduleGraphSync(req
|
|
640
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-deleted");
|
|
639
641
|
res.json({ ok: true });
|
|
640
642
|
});
|
|
641
643
|
// POST /api/projects/:projectId/pm/comments/:itemId
|
|
642
644
|
router.post("/comments/:itemId", async (req, res) => {
|
|
643
|
-
const project = await verifyProject(req.user.userId, req
|
|
645
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
644
646
|
if (!project) {
|
|
645
647
|
res.status(404).json({ error: "Project not found" });
|
|
646
648
|
return;
|
|
@@ -651,7 +653,7 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
651
653
|
return;
|
|
652
654
|
}
|
|
653
655
|
const result = runPm({
|
|
654
|
-
args: ["comments", req
|
|
656
|
+
args: ["comments", routeParam(req, "itemId"), text.trim()],
|
|
655
657
|
userId: project.ownerUserId,
|
|
656
658
|
slug: project.slug,
|
|
657
659
|
jsonOutput: true,
|
|
@@ -664,13 +666,13 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
664
666
|
});
|
|
665
667
|
// GET /api/projects/:projectId/pm/comments/:itemId
|
|
666
668
|
router.get("/comments/:itemId", async (req, res) => {
|
|
667
|
-
const project = await verifyProject(req.user.userId, req
|
|
669
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
668
670
|
if (!project) {
|
|
669
671
|
res.status(404).json({ error: "Project not found" });
|
|
670
672
|
return;
|
|
671
673
|
}
|
|
672
674
|
const result = runPm({
|
|
673
|
-
args: ["comments", req
|
|
675
|
+
args: ["comments", routeParam(req, "itemId")],
|
|
674
676
|
userId: project.ownerUserId,
|
|
675
677
|
slug: project.slug,
|
|
676
678
|
jsonOutput: true,
|
|
@@ -679,13 +681,13 @@ router.get("/comments/:itemId", async (req, res) => {
|
|
|
679
681
|
});
|
|
680
682
|
// GET /api/projects/:projectId/pm/notes/:itemId
|
|
681
683
|
router.get("/notes/:itemId", async (req, res) => {
|
|
682
|
-
const project = await verifyProject(req.user.userId, req
|
|
684
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
683
685
|
if (!project) {
|
|
684
686
|
res.status(404).json({ error: "Project not found" });
|
|
685
687
|
return;
|
|
686
688
|
}
|
|
687
689
|
const result = runPm({
|
|
688
|
-
args: ["notes", req
|
|
690
|
+
args: ["notes", routeParam(req, "itemId")],
|
|
689
691
|
userId: project.ownerUserId,
|
|
690
692
|
slug: project.slug,
|
|
691
693
|
jsonOutput: true,
|
|
@@ -694,7 +696,7 @@ router.get("/notes/:itemId", async (req, res) => {
|
|
|
694
696
|
});
|
|
695
697
|
// POST /api/projects/:projectId/pm/notes/:itemId
|
|
696
698
|
router.post("/notes/:itemId", async (req, res) => {
|
|
697
|
-
const project = await verifyProject(req.user.userId, req
|
|
699
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
698
700
|
if (!project) {
|
|
699
701
|
res.status(404).json({ error: "Project not found" });
|
|
700
702
|
return;
|
|
@@ -705,7 +707,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
705
707
|
return;
|
|
706
708
|
}
|
|
707
709
|
const result = runPm({
|
|
708
|
-
args: ["notes", req
|
|
710
|
+
args: ["notes", routeParam(req, "itemId"), text.trim()],
|
|
709
711
|
userId: project.ownerUserId,
|
|
710
712
|
slug: project.slug,
|
|
711
713
|
jsonOutput: true,
|
|
@@ -718,7 +720,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
718
720
|
});
|
|
719
721
|
// GET /api/projects/:projectId/pm/context
|
|
720
722
|
router.get("/context", async (req, res) => {
|
|
721
|
-
const project = await verifyProject(req.user.userId, req
|
|
723
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
722
724
|
if (!project) {
|
|
723
725
|
res.status(404).json({ error: "Project not found" });
|
|
724
726
|
return;
|
|
@@ -736,7 +738,7 @@ router.get("/context", async (req, res) => {
|
|
|
736
738
|
});
|
|
737
739
|
// GET /api/projects/:projectId/pm/activity
|
|
738
740
|
router.get("/activity", async (req, res) => {
|
|
739
|
-
const project = await verifyProject(req.user.userId, req
|
|
741
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
740
742
|
if (!project) {
|
|
741
743
|
res.status(404).json({ error: "Project not found" });
|
|
742
744
|
return;
|
|
@@ -750,7 +752,7 @@ router.get("/activity", async (req, res) => {
|
|
|
750
752
|
});
|
|
751
753
|
// GET /api/projects/:projectId/pm/stats
|
|
752
754
|
router.get("/stats", async (req, res) => {
|
|
753
|
-
const project = await verifyProject(req.user.userId, req
|
|
755
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
754
756
|
if (!project) {
|
|
755
757
|
res.status(404).json({ error: "Project not found" });
|
|
756
758
|
return;
|
|
@@ -765,7 +767,7 @@ router.get("/stats", async (req, res) => {
|
|
|
765
767
|
});
|
|
766
768
|
// GET /api/projects/:projectId/pm/aggregate
|
|
767
769
|
router.get("/aggregate", async (req, res) => {
|
|
768
|
-
const project = await verifyProject(req.user.userId, req
|
|
770
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
769
771
|
if (!project) {
|
|
770
772
|
res.status(404).json({ error: "Project not found" });
|
|
771
773
|
return;
|
|
@@ -780,7 +782,7 @@ router.get("/aggregate", async (req, res) => {
|
|
|
780
782
|
});
|
|
781
783
|
// POST /api/projects/:projectId/pm/search
|
|
782
784
|
router.post("/search", async (req, res) => {
|
|
783
|
-
const project = await verifyProject(req.user.userId, req
|
|
785
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
784
786
|
if (!project) {
|
|
785
787
|
res.status(404).json({ error: "Project not found" });
|
|
786
788
|
return;
|
|
@@ -809,7 +811,7 @@ router.post("/search", async (req, res) => {
|
|
|
809
811
|
});
|
|
810
812
|
// GET /api/projects/:projectId/pm/calendar
|
|
811
813
|
router.get("/calendar", async (req, res) => {
|
|
812
|
-
const project = await verifyProject(req.user.userId, req
|
|
814
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
813
815
|
if (!project) {
|
|
814
816
|
res.status(404).json({ error: "Project not found" });
|
|
815
817
|
return;
|
|
@@ -822,9 +824,51 @@ router.get("/calendar", async (req, res) => {
|
|
|
822
824
|
});
|
|
823
825
|
res.json(result.ok ? (result.parsed || {}) : { events: [] });
|
|
824
826
|
});
|
|
827
|
+
// GET /api/projects/:projectId/pm/calendar.ics
|
|
828
|
+
// RFC 5545 iCalendar feed of item deadlines, for subscribing in Google
|
|
829
|
+
// Calendar / Outlook / Apple Calendar. Reuses the same list-all read as the
|
|
830
|
+
// calendar view. Auth works via the usual token (header/cookie) or a
|
|
831
|
+
// `?token=` query param, since calendar clients cannot send cookies.
|
|
832
|
+
router.get("/calendar.ics", async (req, res) => {
|
|
833
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
834
|
+
if (!project) {
|
|
835
|
+
res.status(404).json({ error: "Project not found" });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const listed = runPm({
|
|
839
|
+
args: ["list-all", "--json"],
|
|
840
|
+
userId: project.ownerUserId,
|
|
841
|
+
slug: project.slug,
|
|
842
|
+
jsonOutput: true,
|
|
843
|
+
});
|
|
844
|
+
if (!listed.ok) {
|
|
845
|
+
res.status(502).json({ error: listed.stderr || "Failed to load items" });
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const items = itemsFromListAll(listed.parsed)
|
|
849
|
+
.filter((i) => Boolean(i.deadline))
|
|
850
|
+
.map((i) => ({
|
|
851
|
+
id: i.id,
|
|
852
|
+
title: i.title,
|
|
853
|
+
type: i.type,
|
|
854
|
+
status: i.status,
|
|
855
|
+
priority: i.priority,
|
|
856
|
+
deadline: i.deadline,
|
|
857
|
+
assignee: i.assignee,
|
|
858
|
+
tags: i.tags,
|
|
859
|
+
}));
|
|
860
|
+
const ics = buildIcsCalendar(items, {
|
|
861
|
+
calendarName: `pm · ${project.slug}`,
|
|
862
|
+
uidDomain: `${project.slug}.pm-web`,
|
|
863
|
+
});
|
|
864
|
+
res.setHeader("Content-Type", "text/calendar; charset=utf-8");
|
|
865
|
+
res.setHeader("Content-Disposition", `attachment; filename="pm-${project.slug}.ics"`);
|
|
866
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
867
|
+
res.send(ics);
|
|
868
|
+
});
|
|
825
869
|
// GET /api/projects/:projectId/pm/health
|
|
826
870
|
router.get("/health", async (req, res) => {
|
|
827
|
-
const project = await verifyProject(req.user.userId, req
|
|
871
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
828
872
|
if (!project) {
|
|
829
873
|
res.status(404).json({ error: "Project not found" });
|
|
830
874
|
return;
|
|
@@ -839,7 +883,7 @@ router.get("/health", async (req, res) => {
|
|
|
839
883
|
});
|
|
840
884
|
// POST /api/projects/:projectId/pm/append/:itemId
|
|
841
885
|
router.post("/append/:itemId", async (req, res) => {
|
|
842
|
-
const project = await verifyProject(req.user.userId, req
|
|
886
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
843
887
|
if (!project) {
|
|
844
888
|
res.status(404).json({ error: "Project not found" });
|
|
845
889
|
return;
|
|
@@ -850,7 +894,7 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
850
894
|
return;
|
|
851
895
|
}
|
|
852
896
|
const result = runPm({
|
|
853
|
-
args: ["append", req
|
|
897
|
+
args: ["append", routeParam(req, "itemId"), text.trim()],
|
|
854
898
|
userId: project.ownerUserId,
|
|
855
899
|
slug: project.slug,
|
|
856
900
|
jsonOutput: true,
|
|
@@ -859,18 +903,18 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
859
903
|
res.status(400).json({ error: result.stderr || "Failed to append" });
|
|
860
904
|
return;
|
|
861
905
|
}
|
|
862
|
-
scheduleGraphSync(req
|
|
906
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-appended");
|
|
863
907
|
res.json(result.parsed || { ok: true });
|
|
864
908
|
});
|
|
865
909
|
// GET /api/projects/:projectId/pm/history/:itemId
|
|
866
910
|
router.get("/history/:itemId", async (req, res) => {
|
|
867
|
-
const project = await verifyProject(req.user.userId, req
|
|
911
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
868
912
|
if (!project) {
|
|
869
913
|
res.status(404).json({ error: "Project not found" });
|
|
870
914
|
return;
|
|
871
915
|
}
|
|
872
916
|
const result = runPm({
|
|
873
|
-
args: ["history", req
|
|
917
|
+
args: ["history", routeParam(req, "itemId")],
|
|
874
918
|
userId: project.ownerUserId,
|
|
875
919
|
slug: project.slug,
|
|
876
920
|
jsonOutput: true,
|
|
@@ -879,13 +923,13 @@ router.get("/history/:itemId", async (req, res) => {
|
|
|
879
923
|
});
|
|
880
924
|
// GET /api/projects/:projectId/pm/deps/:itemId
|
|
881
925
|
router.get("/deps/:itemId", async (req, res) => {
|
|
882
|
-
const project = await verifyProject(req.user.userId, req
|
|
926
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
883
927
|
if (!project) {
|
|
884
928
|
res.status(404).json({ error: "Project not found" });
|
|
885
929
|
return;
|
|
886
930
|
}
|
|
887
931
|
const result = runPm({
|
|
888
|
-
args: ["deps", req
|
|
932
|
+
args: ["deps", routeParam(req, "itemId")],
|
|
889
933
|
userId: project.ownerUserId,
|
|
890
934
|
slug: project.slug,
|
|
891
935
|
jsonOutput: true,
|
|
@@ -894,7 +938,7 @@ router.get("/deps/:itemId", async (req, res) => {
|
|
|
894
938
|
});
|
|
895
939
|
// POST /api/projects/:projectId/pm/deps/:itemId
|
|
896
940
|
router.post("/deps/:itemId", async (req, res) => {
|
|
897
|
-
const project = await verifyProject(req.user.userId, req
|
|
941
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
898
942
|
if (!project) {
|
|
899
943
|
res.status(404).json({ error: "Project not found" });
|
|
900
944
|
return;
|
|
@@ -906,7 +950,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
906
950
|
}
|
|
907
951
|
const depRel = normalizeDependencyKind(rel);
|
|
908
952
|
const result = runPm({
|
|
909
|
-
args: ["update", req
|
|
953
|
+
args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
|
|
910
954
|
userId: project.ownerUserId,
|
|
911
955
|
slug: project.slug,
|
|
912
956
|
jsonOutput: true,
|
|
@@ -915,9 +959,9 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
915
959
|
res.status(400).json({ error: result.stderr || "Failed to add dependency" });
|
|
916
960
|
return;
|
|
917
961
|
}
|
|
918
|
-
scheduleGraphSync(req
|
|
919
|
-
broadcastDependencyEvent(req
|
|
920
|
-
from: req
|
|
962
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-added");
|
|
963
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
964
|
+
from: routeParam(req, "itemId"),
|
|
921
965
|
to: targetId.trim(),
|
|
922
966
|
rel: depRel,
|
|
923
967
|
userId: req.user.userId,
|
|
@@ -926,7 +970,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
926
970
|
});
|
|
927
971
|
// DELETE /api/projects/:projectId/pm/deps/:itemId
|
|
928
972
|
router.delete("/deps/:itemId", async (req, res) => {
|
|
929
|
-
const project = await verifyProject(req.user.userId, req
|
|
973
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
930
974
|
if (!project) {
|
|
931
975
|
res.status(404).json({ error: "Project not found" });
|
|
932
976
|
return;
|
|
@@ -939,7 +983,7 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
939
983
|
const depRel = normalizeDependencyKind(rel || "relates_to");
|
|
940
984
|
const selector = `id=${targetId.trim()},kind=${depRel}`;
|
|
941
985
|
const result = runPm({
|
|
942
|
-
args: ["update", req
|
|
986
|
+
args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
|
|
943
987
|
userId: project.ownerUserId,
|
|
944
988
|
slug: project.slug,
|
|
945
989
|
jsonOutput: true,
|
|
@@ -948,18 +992,18 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
948
992
|
res.status(400).json({ error: result.stderr || "Failed to remove dependency" });
|
|
949
993
|
return;
|
|
950
994
|
}
|
|
951
|
-
scheduleGraphSync(req
|
|
952
|
-
broadcastDependencyEvent(req
|
|
953
|
-
from: req
|
|
995
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-removed");
|
|
996
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
997
|
+
from: routeParam(req, "itemId"),
|
|
954
998
|
to: targetId.trim(),
|
|
955
999
|
rel: depRel,
|
|
956
1000
|
userId: req.user.userId,
|
|
957
1001
|
});
|
|
958
|
-
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 });
|
|
959
1003
|
});
|
|
960
1004
|
// POST /api/projects/:projectId/pm/rel — Create a relationship between two items
|
|
961
1005
|
router.post("/rel", async (req, res) => {
|
|
962
|
-
const project = await verifyProject(req.user.userId, req
|
|
1006
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
963
1007
|
if (!project) {
|
|
964
1008
|
res.status(404).json({ error: "Project not found" });
|
|
965
1009
|
return;
|
|
@@ -980,8 +1024,8 @@ router.post("/rel", async (req, res) => {
|
|
|
980
1024
|
res.status(400).json({ error: result.stderr || "Failed to create relationship" });
|
|
981
1025
|
return;
|
|
982
1026
|
}
|
|
983
|
-
scheduleGraphSync(req
|
|
984
|
-
broadcastDependencyEvent(req
|
|
1027
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-created");
|
|
1028
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
985
1029
|
from: from.trim(),
|
|
986
1030
|
to: to.trim(),
|
|
987
1031
|
rel: depRel,
|
|
@@ -991,7 +1035,7 @@ router.post("/rel", async (req, res) => {
|
|
|
991
1035
|
});
|
|
992
1036
|
// DELETE /api/projects/:projectId/pm/rel — Remove a relationship between two items
|
|
993
1037
|
router.delete("/rel", async (req, res) => {
|
|
994
|
-
const project = await verifyProject(req.user.userId, req
|
|
1038
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
995
1039
|
if (!project) {
|
|
996
1040
|
res.status(404).json({ error: "Project not found" });
|
|
997
1041
|
return;
|
|
@@ -1013,8 +1057,8 @@ router.delete("/rel", async (req, res) => {
|
|
|
1013
1057
|
res.status(400).json({ error: result.stderr || "Failed to remove relationship" });
|
|
1014
1058
|
return;
|
|
1015
1059
|
}
|
|
1016
|
-
scheduleGraphSync(req
|
|
1017
|
-
broadcastDependencyEvent(req
|
|
1060
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-removed");
|
|
1061
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
1018
1062
|
from: from.trim(),
|
|
1019
1063
|
to: to.trim(),
|
|
1020
1064
|
rel: depRel,
|
|
@@ -1024,7 +1068,7 @@ router.delete("/rel", async (req, res) => {
|
|
|
1024
1068
|
});
|
|
1025
1069
|
// GET /api/projects/:projectId/pm/graph
|
|
1026
1070
|
router.get("/graph", async (req, res) => {
|
|
1027
|
-
const project = await verifyProject(req.user.userId, req
|
|
1071
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1028
1072
|
if (!project) {
|
|
1029
1073
|
res.status(404).json({ error: "Project not found" });
|
|
1030
1074
|
return;
|
|
@@ -1052,7 +1096,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1052
1096
|
});
|
|
1053
1097
|
// POST /api/projects/:projectId/pm/graph/sync
|
|
1054
1098
|
router.post("/graph/sync", async (req, res) => {
|
|
1055
|
-
const project = await verifyProject(req.user.userId, req
|
|
1099
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1056
1100
|
if (!project) {
|
|
1057
1101
|
res.status(404).json({ error: "Project not found" });
|
|
1058
1102
|
return;
|
|
@@ -1060,21 +1104,21 @@ router.post("/graph/sync", async (req, res) => {
|
|
|
1060
1104
|
try {
|
|
1061
1105
|
const syncResult = await syncProjectGraph(project);
|
|
1062
1106
|
const payload = { reason: "manual-sync", ...syncResult, projectKey: graphProjectKey(project), source: "pm-web" };
|
|
1063
|
-
broadcastProjectEvent(req
|
|
1107
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1064
1108
|
type: "graph-synced",
|
|
1065
1109
|
data: payload,
|
|
1066
1110
|
});
|
|
1067
1111
|
res.json({ ok: true, ...payload });
|
|
1068
1112
|
}
|
|
1069
1113
|
catch (err) {
|
|
1070
|
-
broadcastProjectEvent(req
|
|
1114
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1071
1115
|
type: "graph-sync-failed",
|
|
1072
1116
|
data: {
|
|
1073
1117
|
reason: "manual-sync",
|
|
1074
1118
|
error: err instanceof Error ? err.message : String(err),
|
|
1075
1119
|
},
|
|
1076
1120
|
});
|
|
1077
|
-
broadcastProjectEvent(req
|
|
1121
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1078
1122
|
type: "graph_sync_failed",
|
|
1079
1123
|
data: {
|
|
1080
1124
|
reason: "manual-sync",
|
|
@@ -1086,12 +1130,12 @@ router.post("/graph/sync", async (req, res) => {
|
|
|
1086
1130
|
});
|
|
1087
1131
|
// GET /api/projects/:projectId/pm/graph/neighbors/:nodeId
|
|
1088
1132
|
router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
1089
|
-
const project = await verifyProject(req.user.userId, req
|
|
1133
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1090
1134
|
if (!project) {
|
|
1091
1135
|
res.status(404).json({ error: "Project not found" });
|
|
1092
1136
|
return;
|
|
1093
1137
|
}
|
|
1094
|
-
const nodeId = req
|
|
1138
|
+
const nodeId = routeParam(req, "nodeId");
|
|
1095
1139
|
if (!nodeId) {
|
|
1096
1140
|
res.status(400).json({ error: "nodeId is required" });
|
|
1097
1141
|
return;
|
|
@@ -1117,7 +1161,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
|
1117
1161
|
});
|
|
1118
1162
|
// POST /api/projects/:projectId/pm/graph/query
|
|
1119
1163
|
router.post("/graph/query", async (req, res) => {
|
|
1120
|
-
const project = await verifyProject(req.user.userId, req
|
|
1164
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1121
1165
|
if (!project) {
|
|
1122
1166
|
res.status(404).json({ error: "Project not found" });
|
|
1123
1167
|
return;
|
|
@@ -1147,13 +1191,13 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1147
1191
|
});
|
|
1148
1192
|
// GET /api/projects/:projectId/pm/learnings/:itemId
|
|
1149
1193
|
router.get("/learnings/:itemId", async (req, res) => {
|
|
1150
|
-
const project = await verifyProject(req.user.userId, req
|
|
1194
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1151
1195
|
if (!project) {
|
|
1152
1196
|
res.status(404).json({ error: "Project not found" });
|
|
1153
1197
|
return;
|
|
1154
1198
|
}
|
|
1155
1199
|
const result = runPm({
|
|
1156
|
-
args: ["learnings", req
|
|
1200
|
+
args: ["learnings", routeParam(req, "itemId")],
|
|
1157
1201
|
userId: project.ownerUserId,
|
|
1158
1202
|
slug: project.slug,
|
|
1159
1203
|
jsonOutput: true,
|
|
@@ -1162,7 +1206,7 @@ router.get("/learnings/:itemId", async (req, res) => {
|
|
|
1162
1206
|
});
|
|
1163
1207
|
// POST /api/projects/:projectId/pm/learnings/:itemId
|
|
1164
1208
|
router.post("/learnings/:itemId", async (req, res) => {
|
|
1165
|
-
const project = await verifyProject(req.user.userId, req
|
|
1209
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1166
1210
|
if (!project) {
|
|
1167
1211
|
res.status(404).json({ error: "Project not found" });
|
|
1168
1212
|
return;
|
|
@@ -1173,7 +1217,7 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1173
1217
|
return;
|
|
1174
1218
|
}
|
|
1175
1219
|
const result = runPm({
|
|
1176
|
-
args: ["learnings", req
|
|
1220
|
+
args: ["learnings", routeParam(req, "itemId"), text.trim()],
|
|
1177
1221
|
userId: project.ownerUserId,
|
|
1178
1222
|
slug: project.slug,
|
|
1179
1223
|
jsonOutput: true,
|
|
@@ -1186,13 +1230,13 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1186
1230
|
});
|
|
1187
1231
|
// POST /api/projects/:projectId/pm/claim/:itemId
|
|
1188
1232
|
router.post("/claim/:itemId", async (req, res) => {
|
|
1189
|
-
const project = await verifyProject(req.user.userId, req
|
|
1233
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1190
1234
|
if (!project) {
|
|
1191
1235
|
res.status(404).json({ error: "Project not found" });
|
|
1192
1236
|
return;
|
|
1193
1237
|
}
|
|
1194
1238
|
const result = runPm({
|
|
1195
|
-
args: ["claim", req
|
|
1239
|
+
args: ["claim", routeParam(req, "itemId")],
|
|
1196
1240
|
userId: project.ownerUserId,
|
|
1197
1241
|
slug: project.slug,
|
|
1198
1242
|
jsonOutput: true,
|
|
@@ -1201,18 +1245,18 @@ router.post("/claim/:itemId", async (req, res) => {
|
|
|
1201
1245
|
res.status(400).json({ error: result.stderr || "Failed to claim item" });
|
|
1202
1246
|
return;
|
|
1203
1247
|
}
|
|
1204
|
-
scheduleGraphSync(req
|
|
1248
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-claimed");
|
|
1205
1249
|
res.json(result.parsed || { ok: true });
|
|
1206
1250
|
});
|
|
1207
1251
|
// POST /api/projects/:projectId/pm/release/:itemId
|
|
1208
1252
|
router.post("/release/:itemId", async (req, res) => {
|
|
1209
|
-
const project = await verifyProject(req.user.userId, req
|
|
1253
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1210
1254
|
if (!project) {
|
|
1211
1255
|
res.status(404).json({ error: "Project not found" });
|
|
1212
1256
|
return;
|
|
1213
1257
|
}
|
|
1214
1258
|
const result = runPm({
|
|
1215
|
-
args: ["release", req
|
|
1259
|
+
args: ["release", routeParam(req, "itemId")],
|
|
1216
1260
|
userId: project.ownerUserId,
|
|
1217
1261
|
slug: project.slug,
|
|
1218
1262
|
jsonOutput: true,
|
|
@@ -1221,18 +1265,18 @@ router.post("/release/:itemId", async (req, res) => {
|
|
|
1221
1265
|
res.status(400).json({ error: result.stderr || "Failed to release item" });
|
|
1222
1266
|
return;
|
|
1223
1267
|
}
|
|
1224
|
-
scheduleGraphSync(req
|
|
1268
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-released");
|
|
1225
1269
|
res.json(result.parsed || { ok: true });
|
|
1226
1270
|
});
|
|
1227
1271
|
// POST /api/projects/:projectId/pm/start-task/:itemId
|
|
1228
1272
|
router.post("/start-task/:itemId", async (req, res) => {
|
|
1229
|
-
const project = await verifyProject(req.user.userId, req
|
|
1273
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1230
1274
|
if (!project) {
|
|
1231
1275
|
res.status(404).json({ error: "Project not found" });
|
|
1232
1276
|
return;
|
|
1233
1277
|
}
|
|
1234
1278
|
const result = runPm({
|
|
1235
|
-
args: ["start-task", req
|
|
1279
|
+
args: ["start-task", routeParam(req, "itemId")],
|
|
1236
1280
|
userId: project.ownerUserId,
|
|
1237
1281
|
slug: project.slug,
|
|
1238
1282
|
jsonOutput: true,
|
|
@@ -1241,18 +1285,18 @@ router.post("/start-task/:itemId", async (req, res) => {
|
|
|
1241
1285
|
res.status(400).json({ error: result.stderr || "Failed to start task" });
|
|
1242
1286
|
return;
|
|
1243
1287
|
}
|
|
1244
|
-
scheduleGraphSync(req
|
|
1288
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-started");
|
|
1245
1289
|
res.json(result.parsed || { ok: true });
|
|
1246
1290
|
});
|
|
1247
1291
|
// POST /api/projects/:projectId/pm/pause-task/:itemId
|
|
1248
1292
|
router.post("/pause-task/:itemId", async (req, res) => {
|
|
1249
|
-
const project = await verifyProject(req.user.userId, req
|
|
1293
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1250
1294
|
if (!project) {
|
|
1251
1295
|
res.status(404).json({ error: "Project not found" });
|
|
1252
1296
|
return;
|
|
1253
1297
|
}
|
|
1254
1298
|
const result = runPm({
|
|
1255
|
-
args: ["pause-task", req
|
|
1299
|
+
args: ["pause-task", routeParam(req, "itemId")],
|
|
1256
1300
|
userId: project.ownerUserId,
|
|
1257
1301
|
slug: project.slug,
|
|
1258
1302
|
jsonOutput: true,
|
|
@@ -1261,18 +1305,18 @@ router.post("/pause-task/:itemId", async (req, res) => {
|
|
|
1261
1305
|
res.status(400).json({ error: result.stderr || "Failed to pause task" });
|
|
1262
1306
|
return;
|
|
1263
1307
|
}
|
|
1264
|
-
scheduleGraphSync(req
|
|
1308
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-paused");
|
|
1265
1309
|
res.json(result.parsed || { ok: true });
|
|
1266
1310
|
});
|
|
1267
1311
|
// GET /api/projects/:projectId/pm/tests/:itemId
|
|
1268
1312
|
router.get("/tests/:itemId", async (req, res) => {
|
|
1269
|
-
const project = await verifyProject(req.user.userId, req
|
|
1313
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1270
1314
|
if (!project) {
|
|
1271
1315
|
res.status(404).json({ error: "Project not found" });
|
|
1272
1316
|
return;
|
|
1273
1317
|
}
|
|
1274
1318
|
const result = runPm({
|
|
1275
|
-
args: ["test", req
|
|
1319
|
+
args: ["test", routeParam(req, "itemId")],
|
|
1276
1320
|
userId: project.ownerUserId,
|
|
1277
1321
|
slug: project.slug,
|
|
1278
1322
|
jsonOutput: true,
|
|
@@ -1281,7 +1325,7 @@ router.get("/tests/:itemId", async (req, res) => {
|
|
|
1281
1325
|
});
|
|
1282
1326
|
// POST /api/projects/:projectId/pm/tests/:itemId
|
|
1283
1327
|
router.post("/tests/:itemId", async (req, res) => {
|
|
1284
|
-
const project = await verifyProject(req.user.userId, req
|
|
1328
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1285
1329
|
if (!project) {
|
|
1286
1330
|
res.status(404).json({ error: "Project not found" });
|
|
1287
1331
|
return;
|
|
@@ -1291,7 +1335,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1291
1335
|
res.status(400).json({ error: "Test command is required" });
|
|
1292
1336
|
return;
|
|
1293
1337
|
}
|
|
1294
|
-
const args = ["test", req
|
|
1338
|
+
const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
|
|
1295
1339
|
if (description)
|
|
1296
1340
|
args.push("--description", description.trim());
|
|
1297
1341
|
const result = runPm({
|
|
@@ -1308,7 +1352,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1308
1352
|
});
|
|
1309
1353
|
// GET /api/projects/:projectId/pm/dedupe-audit
|
|
1310
1354
|
router.get("/dedupe-audit", async (req, res) => {
|
|
1311
|
-
const project = await verifyProject(req.user.userId, req
|
|
1355
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1312
1356
|
if (!project) {
|
|
1313
1357
|
res.status(404).json({ error: "Project not found" });
|
|
1314
1358
|
return;
|
|
@@ -1323,7 +1367,7 @@ router.get("/dedupe-audit", async (req, res) => {
|
|
|
1323
1367
|
});
|
|
1324
1368
|
// GET /api/projects/:projectId/pm/validate
|
|
1325
1369
|
router.get("/validate", async (req, res) => {
|
|
1326
|
-
const project = await verifyProject(req.user.userId, req
|
|
1370
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1327
1371
|
if (!project) {
|
|
1328
1372
|
res.status(404).json({ error: "Project not found" });
|
|
1329
1373
|
return;
|
|
@@ -1338,7 +1382,7 @@ router.get("/validate", async (req, res) => {
|
|
|
1338
1382
|
});
|
|
1339
1383
|
// POST /api/projects/:projectId/pm/restore/:itemId
|
|
1340
1384
|
router.post("/restore/:itemId", async (req, res) => {
|
|
1341
|
-
const project = await verifyProject(req.user.userId, req
|
|
1385
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1342
1386
|
if (!project) {
|
|
1343
1387
|
res.status(404).json({ error: "Project not found" });
|
|
1344
1388
|
return;
|
|
@@ -1349,7 +1393,7 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1349
1393
|
return;
|
|
1350
1394
|
}
|
|
1351
1395
|
const result = runPm({
|
|
1352
|
-
args: ["restore", req
|
|
1396
|
+
args: ["restore", routeParam(req, "itemId"), target.trim()],
|
|
1353
1397
|
userId: project.ownerUserId,
|
|
1354
1398
|
slug: project.slug,
|
|
1355
1399
|
jsonOutput: true,
|
|
@@ -1358,12 +1402,12 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1358
1402
|
res.status(400).json({ error: result.stderr || "Failed to restore item" });
|
|
1359
1403
|
return;
|
|
1360
1404
|
}
|
|
1361
|
-
scheduleGraphSync(req
|
|
1405
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "item-restored");
|
|
1362
1406
|
res.json(result.parsed || { ok: true });
|
|
1363
1407
|
});
|
|
1364
1408
|
// POST /api/projects/:projectId/pm/close-task/:itemId
|
|
1365
1409
|
router.post("/close-task/:itemId", async (req, res) => {
|
|
1366
|
-
const project = await verifyProject(req.user.userId, req
|
|
1410
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1367
1411
|
if (!project) {
|
|
1368
1412
|
res.status(404).json({ error: "Project not found" });
|
|
1369
1413
|
return;
|
|
@@ -1374,7 +1418,7 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1374
1418
|
return;
|
|
1375
1419
|
}
|
|
1376
1420
|
const result = runPm({
|
|
1377
|
-
args: ["close-task", req
|
|
1421
|
+
args: ["close-task", routeParam(req, "itemId"), reason.trim()],
|
|
1378
1422
|
userId: project.ownerUserId,
|
|
1379
1423
|
slug: project.slug,
|
|
1380
1424
|
jsonOutput: true,
|
|
@@ -1383,12 +1427,12 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1383
1427
|
res.status(400).json({ error: result.stderr || "Failed to close task" });
|
|
1384
1428
|
return;
|
|
1385
1429
|
}
|
|
1386
|
-
scheduleGraphSync(req
|
|
1430
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "task-closed");
|
|
1387
1431
|
res.json(result.parsed || { ok: true });
|
|
1388
1432
|
});
|
|
1389
1433
|
// POST /api/projects/:projectId/pm/reindex
|
|
1390
1434
|
router.post("/reindex", async (req, res) => {
|
|
1391
|
-
const project = await verifyProject(req.user.userId, req
|
|
1435
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1392
1436
|
if (!project) {
|
|
1393
1437
|
res.status(404).json({ error: "Project not found" });
|
|
1394
1438
|
return;
|
|
@@ -1411,7 +1455,7 @@ router.post("/reindex", async (req, res) => {
|
|
|
1411
1455
|
});
|
|
1412
1456
|
// POST /api/projects/:projectId/pm/normalize
|
|
1413
1457
|
router.post("/normalize", async (req, res) => {
|
|
1414
|
-
const project = await verifyProject(req.user.userId, req
|
|
1458
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1415
1459
|
if (!project) {
|
|
1416
1460
|
res.status(404).json({ error: "Project not found" });
|
|
1417
1461
|
return;
|
|
@@ -1426,7 +1470,7 @@ router.post("/normalize", async (req, res) => {
|
|
|
1426
1470
|
});
|
|
1427
1471
|
// GET /api/projects/:projectId/pm/comments-audit
|
|
1428
1472
|
router.get("/comments-audit", async (req, res) => {
|
|
1429
|
-
const project = await verifyProject(req.user.userId, req
|
|
1473
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1430
1474
|
if (!project) {
|
|
1431
1475
|
res.status(404).json({ error: "Project not found" });
|
|
1432
1476
|
return;
|
|
@@ -1441,7 +1485,7 @@ router.get("/comments-audit", async (req, res) => {
|
|
|
1441
1485
|
});
|
|
1442
1486
|
// POST /api/projects/:projectId/pm/files/:itemId
|
|
1443
1487
|
router.post("/files/:itemId", async (req, res) => {
|
|
1444
|
-
const project = await verifyProject(req.user.userId, req
|
|
1488
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1445
1489
|
if (!project) {
|
|
1446
1490
|
res.status(404).json({ error: "Project not found" });
|
|
1447
1491
|
return;
|
|
@@ -1454,7 +1498,7 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1454
1498
|
let addVal = `path=${filePath.trim()}`;
|
|
1455
1499
|
if (scope)
|
|
1456
1500
|
addVal += `,scope=${scope}`;
|
|
1457
|
-
const args = ["files", req
|
|
1501
|
+
const args = ["files", routeParam(req, "itemId"), "--add", addVal];
|
|
1458
1502
|
const result = runPm({
|
|
1459
1503
|
args,
|
|
1460
1504
|
userId: project.ownerUserId,
|
|
@@ -1465,18 +1509,18 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1465
1509
|
res.status(400).json({ error: result.stderr || "Failed to link file" });
|
|
1466
1510
|
return;
|
|
1467
1511
|
}
|
|
1468
|
-
scheduleGraphSync(req
|
|
1512
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "file-linked");
|
|
1469
1513
|
res.status(201).json(result.parsed || { ok: true });
|
|
1470
1514
|
});
|
|
1471
1515
|
// GET /api/projects/:projectId/pm/files/:itemId
|
|
1472
1516
|
router.get("/files/:itemId", async (req, res) => {
|
|
1473
|
-
const project = await verifyProject(req.user.userId, req
|
|
1517
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1474
1518
|
if (!project) {
|
|
1475
1519
|
res.status(404).json({ error: "Project not found" });
|
|
1476
1520
|
return;
|
|
1477
1521
|
}
|
|
1478
1522
|
const result = runPm({
|
|
1479
|
-
args: ["files", req
|
|
1523
|
+
args: ["files", routeParam(req, "itemId")],
|
|
1480
1524
|
userId: project.ownerUserId,
|
|
1481
1525
|
slug: project.slug,
|
|
1482
1526
|
jsonOutput: true,
|
|
@@ -1485,7 +1529,7 @@ router.get("/files/:itemId", async (req, res) => {
|
|
|
1485
1529
|
});
|
|
1486
1530
|
// GET /api/projects/:projectId/pm/guide — list guide topics
|
|
1487
1531
|
router.get("/guide", async (req, res) => {
|
|
1488
|
-
const project = await verifyProject(req.user.userId, req
|
|
1532
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1489
1533
|
if (!project) {
|
|
1490
1534
|
res.status(404).json({ error: "Project not found" });
|
|
1491
1535
|
return;
|
|
@@ -1500,13 +1544,13 @@ router.get("/guide", async (req, res) => {
|
|
|
1500
1544
|
});
|
|
1501
1545
|
// GET /api/projects/:projectId/pm/guide/:topicId — get single guide topic
|
|
1502
1546
|
router.get("/guide/:topicId", async (req, res) => {
|
|
1503
|
-
const project = await verifyProject(req.user.userId, req
|
|
1547
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1504
1548
|
if (!project) {
|
|
1505
1549
|
res.status(404).json({ error: "Project not found" });
|
|
1506
1550
|
return;
|
|
1507
1551
|
}
|
|
1508
1552
|
const result = runPm({
|
|
1509
|
-
args: ["guide", req
|
|
1553
|
+
args: ["guide", routeParam(req, "topicId")],
|
|
1510
1554
|
userId: project.ownerUserId,
|
|
1511
1555
|
slug: project.slug,
|
|
1512
1556
|
jsonOutput: true,
|
|
@@ -1524,7 +1568,7 @@ router.get("/guide/:topicId", async (req, res) => {
|
|
|
1524
1568
|
// ─────────────────────────────────────────────────────────
|
|
1525
1569
|
// GET /api/projects/:projectId/pm/export?format=json|csv|yaml
|
|
1526
1570
|
router.get("/export", async (req, res) => {
|
|
1527
|
-
const project = await verifyProject(req.user.userId, req
|
|
1571
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1528
1572
|
if (!project) {
|
|
1529
1573
|
res.status(404).json({ error: "Project not found" });
|
|
1530
1574
|
return;
|
|
@@ -1624,7 +1668,7 @@ router.get("/export", async (req, res) => {
|
|
|
1624
1668
|
});
|
|
1625
1669
|
// POST /api/projects/:projectId/pm/import — import JSON items
|
|
1626
1670
|
router.post("/import", async (req, res) => {
|
|
1627
|
-
const project = await verifyProject(req.user.userId, req
|
|
1671
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1628
1672
|
if (!project) {
|
|
1629
1673
|
res.status(404).json({ error: "Project not found" });
|
|
1630
1674
|
return;
|
|
@@ -1680,17 +1724,17 @@ router.post("/import", async (req, res) => {
|
|
|
1680
1724
|
errors.push(`item[${i}]: ${result.stderr || "create failed"}`);
|
|
1681
1725
|
}
|
|
1682
1726
|
}
|
|
1683
|
-
broadcastProjectEvent(req
|
|
1727
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1684
1728
|
type: "items-imported",
|
|
1685
1729
|
data: { count: created.length, userId: req.user.userId },
|
|
1686
1730
|
});
|
|
1687
1731
|
if (created.length > 0)
|
|
1688
|
-
scheduleGraphSync(req
|
|
1732
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-imported");
|
|
1689
1733
|
res.json({ created, errors, total: items.length });
|
|
1690
1734
|
});
|
|
1691
1735
|
// POST /api/projects/:projectId/pm/update-many
|
|
1692
1736
|
router.post("/update-many", async (req, res) => {
|
|
1693
|
-
const project = await verifyProject(req.user.userId, req
|
|
1737
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1694
1738
|
if (!project) {
|
|
1695
1739
|
res.status(404).json({ error: "Project not found" });
|
|
1696
1740
|
return;
|
|
@@ -1733,11 +1777,11 @@ router.post("/update-many", async (req, res) => {
|
|
|
1733
1777
|
res.status(400).json({ error: result.stderr || "update-many failed" });
|
|
1734
1778
|
return;
|
|
1735
1779
|
}
|
|
1736
|
-
broadcastProjectEvent(req
|
|
1780
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1737
1781
|
type: "items-bulk-updated",
|
|
1738
1782
|
data: { userId: req.user.userId },
|
|
1739
1783
|
});
|
|
1740
|
-
scheduleGraphSync(req
|
|
1784
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
|
|
1741
1785
|
res.json(result.parsed || {});
|
|
1742
1786
|
});
|
|
1743
1787
|
// POST /api/projects/:projectId/pm/close-many
|
|
@@ -1745,7 +1789,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
1745
1789
|
// Accepts same filter options as update-many plus a required `reason` field.
|
|
1746
1790
|
// Returns { closed_count, failed_count, skipped_count, rows }.
|
|
1747
1791
|
router.post("/close-many", async (req, res) => {
|
|
1748
|
-
const project = await verifyProject(req.user.userId, req
|
|
1792
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1749
1793
|
if (!project) {
|
|
1750
1794
|
res.status(404).json({ error: "Project not found" });
|
|
1751
1795
|
return;
|
|
@@ -1801,11 +1845,11 @@ router.post("/close-many", async (req, res) => {
|
|
|
1801
1845
|
}
|
|
1802
1846
|
}
|
|
1803
1847
|
if (closedCount > 0) {
|
|
1804
|
-
broadcastProjectEvent(req
|
|
1848
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1805
1849
|
type: "items-bulk-updated",
|
|
1806
1850
|
data: { userId: req.user.userId },
|
|
1807
1851
|
});
|
|
1808
|
-
scheduleGraphSync(req
|
|
1852
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
|
|
1809
1853
|
}
|
|
1810
1854
|
res.json({
|
|
1811
1855
|
closed_count: closedCount,
|
|
@@ -1817,13 +1861,13 @@ router.post("/close-many", async (req, res) => {
|
|
|
1817
1861
|
});
|
|
1818
1862
|
// GET /api/projects/:projectId/pm/docs/:itemId
|
|
1819
1863
|
router.get("/docs/:itemId", async (req, res) => {
|
|
1820
|
-
const project = await verifyProject(req.user.userId, req
|
|
1864
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1821
1865
|
if (!project) {
|
|
1822
1866
|
res.status(404).json({ error: "Project not found" });
|
|
1823
1867
|
return;
|
|
1824
1868
|
}
|
|
1825
1869
|
const result = runPm({
|
|
1826
|
-
args: ["docs", req
|
|
1870
|
+
args: ["docs", routeParam(req, "itemId")],
|
|
1827
1871
|
userId: project.ownerUserId,
|
|
1828
1872
|
slug: project.slug,
|
|
1829
1873
|
jsonOutput: true,
|
|
@@ -1832,13 +1876,13 @@ router.get("/docs/:itemId", async (req, res) => {
|
|
|
1832
1876
|
});
|
|
1833
1877
|
// POST /api/projects/:projectId/pm/docs/:itemId
|
|
1834
1878
|
router.post("/docs/:itemId", async (req, res) => {
|
|
1835
|
-
const project = await verifyProject(req.user.userId, req
|
|
1879
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1836
1880
|
if (!project) {
|
|
1837
1881
|
res.status(404).json({ error: "Project not found" });
|
|
1838
1882
|
return;
|
|
1839
1883
|
}
|
|
1840
1884
|
const { path: docPath, scope, note, remove, validatePaths } = req.body;
|
|
1841
|
-
const args = ["docs", req
|
|
1885
|
+
const args = ["docs", routeParam(req, "itemId")];
|
|
1842
1886
|
if (remove) {
|
|
1843
1887
|
args.push("--remove", remove);
|
|
1844
1888
|
}
|
|
@@ -1862,12 +1906,12 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
1862
1906
|
res.status(400).json({ error: result.stderr || "Failed to update docs" });
|
|
1863
1907
|
return;
|
|
1864
1908
|
}
|
|
1865
|
-
scheduleGraphSync(req
|
|
1909
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, "docs-updated");
|
|
1866
1910
|
res.json(result.parsed || { ok: true });
|
|
1867
1911
|
});
|
|
1868
1912
|
// POST /api/projects/:projectId/pm/test-all
|
|
1869
1913
|
router.post("/test-all", async (req, res) => {
|
|
1870
|
-
const project = await verifyProject(req.user.userId, req
|
|
1914
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1871
1915
|
if (!project) {
|
|
1872
1916
|
res.status(404).json({ error: "Project not found" });
|
|
1873
1917
|
return;
|
|
@@ -1889,7 +1933,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
1889
1933
|
});
|
|
1890
1934
|
// GET /api/projects/:projectId/pm/test-runs
|
|
1891
1935
|
router.get("/test-runs", async (req, res) => {
|
|
1892
|
-
const project = await verifyProject(req.user.userId, req
|
|
1936
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1893
1937
|
if (!project) {
|
|
1894
1938
|
res.status(404).json({ error: "Project not found" });
|
|
1895
1939
|
return;
|
|
@@ -1905,7 +1949,7 @@ router.get("/test-runs", async (req, res) => {
|
|
|
1905
1949
|
});
|
|
1906
1950
|
// POST /api/projects/:projectId/pm/gc
|
|
1907
1951
|
router.post("/gc", async (req, res) => {
|
|
1908
|
-
const project = await verifyProject(req.user.userId, req
|
|
1952
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1909
1953
|
if (!project) {
|
|
1910
1954
|
res.status(404).json({ error: "Project not found" });
|
|
1911
1955
|
return;
|
|
@@ -1915,7 +1959,7 @@ router.post("/gc", async (req, res) => {
|
|
|
1915
1959
|
});
|
|
1916
1960
|
// GET /api/projects/:projectId/pm/templates
|
|
1917
1961
|
router.get("/templates", async (req, res) => {
|
|
1918
|
-
const project = await verifyProject(req.user.userId, req
|
|
1962
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1919
1963
|
if (!project) {
|
|
1920
1964
|
res.status(404).json({ error: "Project not found" });
|
|
1921
1965
|
return;
|
|
@@ -1925,17 +1969,17 @@ router.get("/templates", async (req, res) => {
|
|
|
1925
1969
|
});
|
|
1926
1970
|
// GET /api/projects/:projectId/pm/templates/:name
|
|
1927
1971
|
router.get("/templates/:name", async (req, res) => {
|
|
1928
|
-
const project = await verifyProject(req.user.userId, req
|
|
1972
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1929
1973
|
if (!project) {
|
|
1930
1974
|
res.status(404).json({ error: "Project not found" });
|
|
1931
1975
|
return;
|
|
1932
1976
|
}
|
|
1933
|
-
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 });
|
|
1934
1978
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1935
1979
|
});
|
|
1936
1980
|
// GET /api/projects/:projectId/pm/config
|
|
1937
1981
|
router.get("/config", async (req, res) => {
|
|
1938
|
-
const project = await verifyProject(req.user.userId, req
|
|
1982
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1939
1983
|
if (!project) {
|
|
1940
1984
|
res.status(404).json({ error: "Project not found" });
|
|
1941
1985
|
return;
|
|
@@ -1945,23 +1989,23 @@ router.get("/config", async (req, res) => {
|
|
|
1945
1989
|
});
|
|
1946
1990
|
// GET /api/projects/:projectId/pm/config/:key
|
|
1947
1991
|
router.get("/config/:key", async (req, res) => {
|
|
1948
|
-
const project = await verifyProject(req.user.userId, req
|
|
1992
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1949
1993
|
if (!project) {
|
|
1950
1994
|
res.status(404).json({ error: "Project not found" });
|
|
1951
1995
|
return;
|
|
1952
1996
|
}
|
|
1953
|
-
const key = req
|
|
1997
|
+
const key = routeParam(req, "key");
|
|
1954
1998
|
const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1955
1999
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1956
2000
|
});
|
|
1957
2001
|
// PATCH /api/projects/:projectId/pm/config/:key
|
|
1958
2002
|
router.patch("/config/:key", async (req, res) => {
|
|
1959
|
-
const project = await verifyProject(req.user.userId, req
|
|
2003
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1960
2004
|
if (!project) {
|
|
1961
2005
|
res.status(404).json({ error: "Project not found" });
|
|
1962
2006
|
return;
|
|
1963
2007
|
}
|
|
1964
|
-
const key = req
|
|
2008
|
+
const key = routeParam(req, "key");
|
|
1965
2009
|
const body = req.body;
|
|
1966
2010
|
const args = ["config", "project", "set", key];
|
|
1967
2011
|
if (body.value)
|
|
@@ -1977,7 +2021,7 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
1977
2021
|
// These wrap pm list-draft, list-open, etc.
|
|
1978
2022
|
function buildListShortcutRoute(pmCommand) {
|
|
1979
2023
|
return async (req, res) => {
|
|
1980
|
-
const project = await verifyProject(req.user.userId, req
|
|
2024
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
1981
2025
|
if (!project) {
|
|
1982
2026
|
res.status(404).json({ error: "Project not found" });
|
|
1983
2027
|
return;
|
|
@@ -2015,7 +2059,7 @@ router.get("/list-canceled", buildListShortcutRoute("list-canceled"));
|
|
|
2015
2059
|
// ─────────────────────────────────────────────────────────
|
|
2016
2060
|
// POST /api/projects/:projectId/pm/plan
|
|
2017
2061
|
router.post("/plan", async (req, res) => {
|
|
2018
|
-
const project = await verifyProject(req.user.userId, req
|
|
2062
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2019
2063
|
if (!project) {
|
|
2020
2064
|
res.status(404).json({ error: "Project not found" });
|
|
2021
2065
|
return;
|
|
@@ -2041,7 +2085,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2041
2085
|
res.status(400).json({ error: result.stderr || "Failed to create plan" });
|
|
2042
2086
|
return;
|
|
2043
2087
|
}
|
|
2044
|
-
broadcastProjectEvent(req
|
|
2088
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2045
2089
|
type: "item-created",
|
|
2046
2090
|
data: { result: result.parsed, userId: req.user.userId },
|
|
2047
2091
|
});
|
|
@@ -2049,13 +2093,13 @@ router.post("/plan", async (req, res) => {
|
|
|
2049
2093
|
});
|
|
2050
2094
|
// GET /api/projects/:projectId/pm/plan/:planId
|
|
2051
2095
|
router.get("/plan/:planId", async (req, res) => {
|
|
2052
|
-
const project = await verifyProject(req.user.userId, req
|
|
2096
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2053
2097
|
if (!project) {
|
|
2054
2098
|
res.status(404).json({ error: "Project not found" });
|
|
2055
2099
|
return;
|
|
2056
2100
|
}
|
|
2057
2101
|
const result = runPm({
|
|
2058
|
-
args: ["plan", "show", req
|
|
2102
|
+
args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
|
|
2059
2103
|
userId: project.ownerUserId,
|
|
2060
2104
|
slug: project.slug,
|
|
2061
2105
|
jsonOutput: true,
|
|
@@ -2068,13 +2112,13 @@ router.get("/plan/:planId", async (req, res) => {
|
|
|
2068
2112
|
});
|
|
2069
2113
|
// PATCH /api/projects/:projectId/pm/plan/:planId
|
|
2070
2114
|
router.patch("/plan/:planId", async (req, res) => {
|
|
2071
|
-
const project = await verifyProject(req.user.userId, req
|
|
2115
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2072
2116
|
if (!project) {
|
|
2073
2117
|
res.status(404).json({ error: "Project not found" });
|
|
2074
2118
|
return;
|
|
2075
2119
|
}
|
|
2076
2120
|
const { title, description } = req.body;
|
|
2077
|
-
const args = ["update", req
|
|
2121
|
+
const args = ["update", routeParam(req, "planId")];
|
|
2078
2122
|
if (title?.trim())
|
|
2079
2123
|
args.push("--title", title.trim());
|
|
2080
2124
|
if (description !== undefined)
|
|
@@ -2084,21 +2128,21 @@ router.patch("/plan/:planId", async (req, res) => {
|
|
|
2084
2128
|
res.status(400).json({ error: result.stderr || "Failed to update plan" });
|
|
2085
2129
|
return;
|
|
2086
2130
|
}
|
|
2087
|
-
broadcastProjectEvent(req
|
|
2131
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2088
2132
|
type: "item-updated",
|
|
2089
|
-
data: { itemId: req
|
|
2133
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2090
2134
|
});
|
|
2091
2135
|
res.json(result.parsed || {});
|
|
2092
2136
|
});
|
|
2093
2137
|
// DELETE /api/projects/:projectId/pm/plan/:planId
|
|
2094
2138
|
router.delete("/plan/:planId", async (req, res) => {
|
|
2095
|
-
const project = await verifyProject(req.user.userId, req
|
|
2139
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2096
2140
|
if (!project) {
|
|
2097
2141
|
res.status(404).json({ error: "Project not found" });
|
|
2098
2142
|
return;
|
|
2099
2143
|
}
|
|
2100
2144
|
const result = runPm({
|
|
2101
|
-
args: ["delete", req
|
|
2145
|
+
args: ["delete", routeParam(req, "planId")],
|
|
2102
2146
|
userId: project.ownerUserId,
|
|
2103
2147
|
slug: project.slug,
|
|
2104
2148
|
jsonOutput: true,
|
|
@@ -2107,15 +2151,15 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2107
2151
|
res.status(400).json({ error: result.stderr || "Failed to delete plan" });
|
|
2108
2152
|
return;
|
|
2109
2153
|
}
|
|
2110
|
-
broadcastProjectEvent(req
|
|
2154
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2111
2155
|
type: "item-deleted",
|
|
2112
|
-
data: { itemId: req
|
|
2156
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2113
2157
|
});
|
|
2114
2158
|
res.json({ ok: true });
|
|
2115
2159
|
});
|
|
2116
2160
|
// POST /api/projects/:projectId/pm/plan/:planId/steps
|
|
2117
2161
|
router.post("/plan/:planId/steps", async (req, res) => {
|
|
2118
|
-
const project = await verifyProject(req.user.userId, req
|
|
2162
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2119
2163
|
if (!project) {
|
|
2120
2164
|
res.status(404).json({ error: "Project not found" });
|
|
2121
2165
|
return;
|
|
@@ -2125,7 +2169,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2125
2169
|
res.status(400).json({ error: "Title is required" });
|
|
2126
2170
|
return;
|
|
2127
2171
|
}
|
|
2128
|
-
const args = ["plan", "add-step", req
|
|
2172
|
+
const args = ["plan", "add-step", routeParam(req, "planId"), "--title", title.trim()];
|
|
2129
2173
|
if (description)
|
|
2130
2174
|
args.push("--description", description);
|
|
2131
2175
|
if (dependsOn)
|
|
@@ -2135,21 +2179,21 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2135
2179
|
res.status(400).json({ error: result.stderr || "Failed to add step" });
|
|
2136
2180
|
return;
|
|
2137
2181
|
}
|
|
2138
|
-
broadcastProjectEvent(req
|
|
2182
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2139
2183
|
type: "item-updated",
|
|
2140
|
-
data: { itemId: req
|
|
2184
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2141
2185
|
});
|
|
2142
2186
|
res.status(201).json(result.parsed || {});
|
|
2143
2187
|
});
|
|
2144
2188
|
// PATCH /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2145
2189
|
router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2146
|
-
const project = await verifyProject(req.user.userId, req
|
|
2190
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2147
2191
|
if (!project) {
|
|
2148
2192
|
res.status(404).json({ error: "Project not found" });
|
|
2149
2193
|
return;
|
|
2150
2194
|
}
|
|
2151
2195
|
const { title, description } = req.body;
|
|
2152
|
-
const args = ["plan", "update-step", req
|
|
2196
|
+
const args = ["plan", "update-step", routeParam(req, "planId"), routeParam(req, "stepRef")];
|
|
2153
2197
|
if (title)
|
|
2154
2198
|
args.push("--title", title);
|
|
2155
2199
|
if (description)
|
|
@@ -2159,21 +2203,21 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2159
2203
|
res.status(400).json({ error: result.stderr || "Failed to update step" });
|
|
2160
2204
|
return;
|
|
2161
2205
|
}
|
|
2162
|
-
broadcastProjectEvent(req
|
|
2206
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2163
2207
|
type: "item-updated",
|
|
2164
|
-
data: { itemId: req
|
|
2208
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2165
2209
|
});
|
|
2166
2210
|
res.json(result.parsed || {});
|
|
2167
2211
|
});
|
|
2168
2212
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/complete
|
|
2169
2213
|
router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
2170
|
-
const project = await verifyProject(req.user.userId, req
|
|
2214
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2171
2215
|
if (!project) {
|
|
2172
2216
|
res.status(404).json({ error: "Project not found" });
|
|
2173
2217
|
return;
|
|
2174
2218
|
}
|
|
2175
2219
|
const result = runPm({
|
|
2176
|
-
args: ["plan", "complete-step", req
|
|
2220
|
+
args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2177
2221
|
userId: project.ownerUserId,
|
|
2178
2222
|
slug: project.slug,
|
|
2179
2223
|
jsonOutput: true,
|
|
@@ -2182,15 +2226,15 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
|
2182
2226
|
res.status(400).json({ error: result.stderr || "Failed to complete step" });
|
|
2183
2227
|
return;
|
|
2184
2228
|
}
|
|
2185
|
-
broadcastProjectEvent(req
|
|
2229
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2186
2230
|
type: "item-updated",
|
|
2187
|
-
data: { itemId: req
|
|
2231
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2188
2232
|
});
|
|
2189
2233
|
res.json(result.parsed || {});
|
|
2190
2234
|
});
|
|
2191
2235
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/block
|
|
2192
2236
|
router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
2193
|
-
const project = await verifyProject(req.user.userId, req
|
|
2237
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2194
2238
|
if (!project) {
|
|
2195
2239
|
res.status(404).json({ error: "Project not found" });
|
|
2196
2240
|
return;
|
|
@@ -2201,7 +2245,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2201
2245
|
return;
|
|
2202
2246
|
}
|
|
2203
2247
|
const result = runPm({
|
|
2204
|
-
args: ["plan", "block-step", req
|
|
2248
|
+
args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
|
|
2205
2249
|
userId: project.ownerUserId,
|
|
2206
2250
|
slug: project.slug,
|
|
2207
2251
|
jsonOutput: true,
|
|
@@ -2210,21 +2254,21 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2210
2254
|
res.status(400).json({ error: result.stderr || "Failed to block step" });
|
|
2211
2255
|
return;
|
|
2212
2256
|
}
|
|
2213
|
-
broadcastProjectEvent(req
|
|
2257
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2214
2258
|
type: "item-updated",
|
|
2215
|
-
data: { itemId: req
|
|
2259
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2216
2260
|
});
|
|
2217
2261
|
res.json(result.parsed || {});
|
|
2218
2262
|
});
|
|
2219
2263
|
// DELETE /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2220
2264
|
router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2221
|
-
const project = await verifyProject(req.user.userId, req
|
|
2265
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2222
2266
|
if (!project) {
|
|
2223
2267
|
res.status(404).json({ error: "Project not found" });
|
|
2224
2268
|
return;
|
|
2225
2269
|
}
|
|
2226
2270
|
const result = runPm({
|
|
2227
|
-
args: ["plan", "remove-step", req
|
|
2271
|
+
args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2228
2272
|
userId: project.ownerUserId,
|
|
2229
2273
|
slug: project.slug,
|
|
2230
2274
|
jsonOutput: true,
|
|
@@ -2233,21 +2277,21 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2233
2277
|
res.status(400).json({ error: result.stderr || "Failed to remove step" });
|
|
2234
2278
|
return;
|
|
2235
2279
|
}
|
|
2236
|
-
broadcastProjectEvent(req
|
|
2280
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2237
2281
|
type: "item-updated",
|
|
2238
|
-
data: { itemId: req
|
|
2282
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2239
2283
|
});
|
|
2240
2284
|
res.json(result.parsed || {});
|
|
2241
2285
|
});
|
|
2242
2286
|
// POST /api/projects/:projectId/pm/plan/:planId/approve
|
|
2243
2287
|
router.post("/plan/:planId/approve", async (req, res) => {
|
|
2244
|
-
const project = await verifyProject(req.user.userId, req
|
|
2288
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2245
2289
|
if (!project) {
|
|
2246
2290
|
res.status(404).json({ error: "Project not found" });
|
|
2247
2291
|
return;
|
|
2248
2292
|
}
|
|
2249
2293
|
const result = runPm({
|
|
2250
|
-
args: ["plan", "approve", req
|
|
2294
|
+
args: ["plan", "approve", routeParam(req, "planId")],
|
|
2251
2295
|
userId: project.ownerUserId,
|
|
2252
2296
|
slug: project.slug,
|
|
2253
2297
|
jsonOutput: true,
|
|
@@ -2256,21 +2300,21 @@ router.post("/plan/:planId/approve", async (req, res) => {
|
|
|
2256
2300
|
res.status(400).json({ error: result.stderr || "Failed to approve plan" });
|
|
2257
2301
|
return;
|
|
2258
2302
|
}
|
|
2259
|
-
broadcastProjectEvent(req
|
|
2303
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2260
2304
|
type: "item-updated",
|
|
2261
|
-
data: { itemId: req
|
|
2305
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2262
2306
|
});
|
|
2263
2307
|
res.json(result.parsed || {});
|
|
2264
2308
|
});
|
|
2265
2309
|
// POST /api/projects/:projectId/pm/plan/:planId/materialize
|
|
2266
2310
|
router.post("/plan/:planId/materialize", async (req, res) => {
|
|
2267
|
-
const project = await verifyProject(req.user.userId, req
|
|
2311
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2268
2312
|
if (!project) {
|
|
2269
2313
|
res.status(404).json({ error: "Project not found" });
|
|
2270
2314
|
return;
|
|
2271
2315
|
}
|
|
2272
2316
|
const { materializeType, materializeParent, steps } = req.body;
|
|
2273
|
-
const args = ["plan", "materialize", req
|
|
2317
|
+
const args = ["plan", "materialize", routeParam(req, "planId")];
|
|
2274
2318
|
if (materializeType)
|
|
2275
2319
|
args.push("--materialize-type", materializeType);
|
|
2276
2320
|
if (materializeParent)
|
|
@@ -2282,7 +2326,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2282
2326
|
res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2283
2327
|
return;
|
|
2284
2328
|
}
|
|
2285
|
-
broadcastProjectEvent(req
|
|
2329
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2286
2330
|
type: "item-created",
|
|
2287
2331
|
data: { result: result.parsed, userId: req.user.userId },
|
|
2288
2332
|
});
|
|
@@ -2290,7 +2334,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2290
2334
|
});
|
|
2291
2335
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/reorder
|
|
2292
2336
|
router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
2293
|
-
const project = await verifyProject(req.user.userId, req
|
|
2337
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2294
2338
|
if (!project) {
|
|
2295
2339
|
res.status(404).json({ error: "Project not found" });
|
|
2296
2340
|
return;
|
|
@@ -2301,7 +2345,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2301
2345
|
return;
|
|
2302
2346
|
}
|
|
2303
2347
|
const result = runPm({
|
|
2304
|
-
args: ["plan", "reorder-step", req
|
|
2348
|
+
args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
|
|
2305
2349
|
userId: project.ownerUserId,
|
|
2306
2350
|
slug: project.slug,
|
|
2307
2351
|
jsonOutput: true,
|
|
@@ -2310,15 +2354,15 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2310
2354
|
res.status(400).json({ error: result.stderr || "Failed to reorder step" });
|
|
2311
2355
|
return;
|
|
2312
2356
|
}
|
|
2313
|
-
broadcastProjectEvent(req
|
|
2357
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2314
2358
|
type: "item-updated",
|
|
2315
|
-
data: { itemId: req
|
|
2359
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2316
2360
|
});
|
|
2317
2361
|
res.json(result.parsed || {});
|
|
2318
2362
|
});
|
|
2319
2363
|
// POST /api/projects/:projectId/pm/plan/:planId/link
|
|
2320
2364
|
router.post("/plan/:planId/link", async (req, res) => {
|
|
2321
|
-
const project = await verifyProject(req.user.userId, req
|
|
2365
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2322
2366
|
if (!project) {
|
|
2323
2367
|
res.status(404).json({ error: "Project not found" });
|
|
2324
2368
|
return;
|
|
@@ -2328,7 +2372,7 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2328
2372
|
res.status(400).json({ error: "link (item id) is required" });
|
|
2329
2373
|
return;
|
|
2330
2374
|
}
|
|
2331
|
-
const args = ["plan", "link", req
|
|
2375
|
+
const args = ["plan", "link", routeParam(req, "planId"), "--link", link.trim()];
|
|
2332
2376
|
if (linkKind)
|
|
2333
2377
|
args.push("--link-kind", linkKind);
|
|
2334
2378
|
if (linkNote)
|
|
@@ -2340,15 +2384,15 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2340
2384
|
res.status(400).json({ error: result.stderr || "Failed to link plan" });
|
|
2341
2385
|
return;
|
|
2342
2386
|
}
|
|
2343
|
-
broadcastProjectEvent(req
|
|
2387
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2344
2388
|
type: "item-updated",
|
|
2345
|
-
data: { itemId: req
|
|
2389
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2346
2390
|
});
|
|
2347
2391
|
res.status(201).json(result.parsed || {});
|
|
2348
2392
|
});
|
|
2349
2393
|
// DELETE /api/projects/:projectId/pm/plan/:planId/link
|
|
2350
2394
|
router.delete("/plan/:planId/link", async (req, res) => {
|
|
2351
|
-
const project = await verifyProject(req.user.userId, req
|
|
2395
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2352
2396
|
if (!project) {
|
|
2353
2397
|
res.status(404).json({ error: "Project not found" });
|
|
2354
2398
|
return;
|
|
@@ -2358,7 +2402,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2358
2402
|
res.status(400).json({ error: "link (item id) is required" });
|
|
2359
2403
|
return;
|
|
2360
2404
|
}
|
|
2361
|
-
const args = ["plan", "unlink", req
|
|
2405
|
+
const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
|
|
2362
2406
|
if (linkKind)
|
|
2363
2407
|
args.push("--link-kind", linkKind);
|
|
2364
2408
|
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
@@ -2366,9 +2410,9 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2366
2410
|
res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2367
2411
|
return;
|
|
2368
2412
|
}
|
|
2369
|
-
broadcastProjectEvent(req
|
|
2413
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2370
2414
|
type: "item-updated",
|
|
2371
|
-
data: { itemId: req
|
|
2415
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2372
2416
|
});
|
|
2373
2417
|
res.json(result.parsed || {});
|
|
2374
2418
|
});
|
|
@@ -2377,7 +2421,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2377
2421
|
// POST /api/projects/:projectId/pm/upgrade
|
|
2378
2422
|
// Runs pm upgrade --packages-only (never upgrades the CLI itself via the web UI).
|
|
2379
2423
|
router.get("/upgrade", async (req, res) => {
|
|
2380
|
-
const project = await verifyProject(req.user.userId, req
|
|
2424
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2381
2425
|
if (!project) {
|
|
2382
2426
|
res.status(404).json({ error: "Project not found" });
|
|
2383
2427
|
return;
|
|
@@ -2391,7 +2435,7 @@ router.get("/upgrade", async (req, res) => {
|
|
|
2391
2435
|
res.json(result.ok ? (result.parsed || { dryRun: true }) : { error: result.stderr });
|
|
2392
2436
|
});
|
|
2393
2437
|
router.post("/upgrade", async (req, res) => {
|
|
2394
|
-
const project = await verifyProject(req.user.userId, req
|
|
2438
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2395
2439
|
if (!project) {
|
|
2396
2440
|
res.status(404).json({ error: "Project not found" });
|
|
2397
2441
|
return;
|
|
@@ -2412,7 +2456,7 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2412
2456
|
// ─── Presence endpoints ───
|
|
2413
2457
|
// GET /api/projects/:projectId/pm/presence
|
|
2414
2458
|
router.get("/presence", async (req, res) => {
|
|
2415
|
-
const projectId = req
|
|
2459
|
+
const projectId = routeParam(req, "projectId");
|
|
2416
2460
|
res.json({ users: getProjectPresence(projectId) });
|
|
2417
2461
|
});
|
|
2418
2462
|
// PATCH /api/projects/:projectId/pm/presence/:clientId
|
|
@@ -2427,7 +2471,7 @@ router.patch("/presence/:clientId", async (req, res) => {
|
|
|
2427
2471
|
// GET /api/projects/:projectId/pm/events
|
|
2428
2472
|
router.get("/events", async (req, res) => {
|
|
2429
2473
|
try {
|
|
2430
|
-
const project = await verifyProject(req.user.userId, req
|
|
2474
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2431
2475
|
if (!project) {
|
|
2432
2476
|
res.status(404).json({ error: "Project not found" });
|
|
2433
2477
|
return;
|
|
@@ -2440,7 +2484,7 @@ router.get("/events", async (req, res) => {
|
|
|
2440
2484
|
}
|
|
2441
2485
|
setupSSEHeaders(res);
|
|
2442
2486
|
const clientId = uuidv4();
|
|
2443
|
-
const projectId = req
|
|
2487
|
+
const projectId = routeParam(req, "projectId");
|
|
2444
2488
|
const userId = req.user.userId;
|
|
2445
2489
|
// Client sends display name as query param; fall back to email
|
|
2446
2490
|
const displayName = String(req.query["dn"] ?? req.user.email ?? userId);
|
|
@@ -2474,12 +2518,12 @@ router.get("/events", async (req, res) => {
|
|
|
2474
2518
|
// ─── Presence endpoint ───
|
|
2475
2519
|
// GET /api/projects/:projectId/pm/presence
|
|
2476
2520
|
router.get("/presence", async (req, res) => {
|
|
2477
|
-
const project = await verifyProject(req.user.userId, req
|
|
2521
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
2478
2522
|
if (!project) {
|
|
2479
2523
|
res.status(404).json({ error: "Project not found" });
|
|
2480
2524
|
return;
|
|
2481
2525
|
}
|
|
2482
|
-
const users = getProjectPresence(req
|
|
2526
|
+
const users = getProjectPresence(routeParam(req, "projectId"));
|
|
2483
2527
|
res.json({ users });
|
|
2484
2528
|
});
|
|
2485
2529
|
export { router as pmRouter };
|