@unbrained/pm-web 2026.6.2-1 → 2026.6.4
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 +7 -1
- package/README.md +43 -0
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/ical.js +141 -0
- package/dist/ical.js.map +1 -0
- package/dist/routes/pm.js +43 -0
- package/dist/routes/pm.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- 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/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/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2026.6.
|
|
3
|
+
## 2026.6.4 - 2026-06-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add light/auto theme toggle, URL-state item filters, and iCal \(.ics\) calendar export ([pm-web-b6eo](https://github.com/unbraind/pm-web/blob/main/.agents/pm/tasks/pm-web-b6eo.toon))
|
|
8
|
+
|
|
9
|
+
## 2026.06.02-1 - 2026-06-02
|
|
4
10
|
|
|
5
11
|
### Added
|
|
6
12
|
|
package/README.md
CHANGED
|
@@ -166,6 +166,49 @@ extensions):
|
|
|
166
166
|
search over id, title, tags and body.
|
|
167
167
|
- `GET /api/projects/:projectId/pm/schema` — runtime types/statuses (existing).
|
|
168
168
|
- `GET /api/projects/:projectId/pm/graph` — dependency graph (existing).
|
|
169
|
+
- `GET /api/projects/:projectId/pm/calendar.ics` — RFC 5545 iCalendar feed of
|
|
170
|
+
item deadlines (see below).
|
|
169
171
|
|
|
170
172
|
The pure grouping/search helpers live in `src/board.ts` and are unit-tested
|
|
171
173
|
independently of the database.
|
|
174
|
+
|
|
175
|
+
## UI features
|
|
176
|
+
|
|
177
|
+
### Theme (dark / light / auto)
|
|
178
|
+
|
|
179
|
+
The web UI supports three themes — **dark** (default), **light**, and **auto**
|
|
180
|
+
(follows the OS `prefers-color-scheme`). Toggle with the button in the top nav
|
|
181
|
+
or the `t` keyboard shortcut; the choice is persisted to `localStorage`. The
|
|
182
|
+
palette is driven entirely by CSS variables (`public/styles.css`); the toggle
|
|
183
|
+
logic lives in `public/src/theme.ts`.
|
|
184
|
+
|
|
185
|
+
### Shareable filtered views
|
|
186
|
+
|
|
187
|
+
The Items view mirrors its filters into the URL query string, so a filtered
|
|
188
|
+
view is shareable and bookmarkable:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
/items?status=open&type=Feature&priority=1&assignee=alice&tag=release
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Filterable dimensions: `status`, `type`, `priority`, `sprint`, `release`,
|
|
195
|
+
`assignee`, `tag`. Opening such a URL restores the filters; use the **Copy
|
|
196
|
+
link** button to copy the current filtered view. The pure (de)serialization
|
|
197
|
+
helpers live in `public/src/filters.ts` and are unit-tested.
|
|
198
|
+
|
|
199
|
+
### Calendar export (iCal / .ics)
|
|
200
|
+
|
|
201
|
+
`GET /api/projects/:projectId/pm/calendar.ics` returns an RFC 5545 `VCALENDAR`
|
|
202
|
+
with a `VEVENT` per item that has a deadline, suitable for **subscribing** in
|
|
203
|
+
Google Calendar, Outlook, or Apple Calendar. Date-only deadlines become all-day
|
|
204
|
+
events; items without a deadline are skipped. Use the **Export .ics** button on
|
|
205
|
+
the Calendar view to download, or subscribe to the feed URL. Because calendar
|
|
206
|
+
clients cannot send cookies, the feed also accepts the JWT via a `?token=<jwt>`
|
|
207
|
+
query parameter (in addition to the usual `Authorization: Bearer` header /
|
|
208
|
+
cookie). The pure generator lives in `src/ical.ts` and is unit-tested.
|
|
209
|
+
|
|
210
|
+
### Keyboard shortcuts
|
|
211
|
+
|
|
212
|
+
Press `?` for the in-app shortcuts overlay. Highlights: `Ctrl/⌘+K` global
|
|
213
|
+
search, `/` focus search, `n`/`c` new item, `t` cycle theme, `a` activity,
|
|
214
|
+
`g i` items, `g g` graph, `g s` search, `g c` calendar, `Esc` close modal.
|
package/dist/auth.js
CHANGED
|
@@ -15,6 +15,12 @@ export function extractToken(req) {
|
|
|
15
15
|
const cookie = req.cookies?.pm_token;
|
|
16
16
|
if (cookie)
|
|
17
17
|
return cookie;
|
|
18
|
+
// Allow a `?token=` query param. Calendar clients subscribing to an .ics
|
|
19
|
+
// feed cannot send cookies or an Authorization header, so the feed URL
|
|
20
|
+
// carries the JWT directly. Additive — header/cookie still take precedence.
|
|
21
|
+
const queryToken = req.query?.["token"];
|
|
22
|
+
if (typeof queryToken === "string" && queryToken)
|
|
23
|
+
return queryToken;
|
|
18
24
|
return null;
|
|
19
25
|
}
|
|
20
26
|
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAG/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,kCAAkC,CAAC;AAChF,MAAM,WAAW,GAAG,KAAK,CAAC;AAO1B,MAAM,UAAU,SAAS,CAAC,OAAmB;IAC3C,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAe,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC7C,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,MAAM,GAAI,GAAsD,CAAC,OAAO,EAAE,QAAQ,CAAC;IACzF,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAG/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,kCAAkC,CAAC;AAChF,MAAM,WAAW,GAAG,KAAK,CAAC;AAO1B,MAAM,UAAU,SAAS,CAAC,OAAmB;IAC3C,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAe,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC7C,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,MAAM,GAAI,GAAsD,CAAC,OAAO,EAAE,QAAQ,CAAC;IACzF,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,yEAAyE;IACzE,uEAAuE;IACvE,4EAA4E;IAC5E,MAAM,UAAU,GAAI,GAAG,CAAC,KAA6C,EAAE,CAAC,OAAO,CAAC,CAAC;IACjF,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/ical.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Pure, dependency-free RFC 5545 (iCalendar) generation for pm items.
|
|
2
|
+
//
|
|
3
|
+
// Kept in its own module (no db/express/neo4j imports) so it is unit-testable
|
|
4
|
+
// without booting the server or a database. The pm route layer reads items via
|
|
5
|
+
// the pm CLI and hands the relevant fields here.
|
|
6
|
+
// Escape a text value per RFC 5545 §3.3.11 (TEXT): backslash, semicolon,
|
|
7
|
+
// comma and newlines must be escaped.
|
|
8
|
+
export function icsEscapeText(value) {
|
|
9
|
+
return value
|
|
10
|
+
.replace(/\\/g, "\\\\")
|
|
11
|
+
.replace(/;/g, "\\;")
|
|
12
|
+
.replace(/,/g, "\\,")
|
|
13
|
+
.replace(/\r\n|\r|\n/g, "\\n");
|
|
14
|
+
}
|
|
15
|
+
// Fold long content lines to 75 octets per RFC 5545 §3.1. Continuation lines
|
|
16
|
+
// begin with a single space. We fold on character boundaries which is safe for
|
|
17
|
+
// the ASCII-dominant content pm produces.
|
|
18
|
+
export function foldLine(line) {
|
|
19
|
+
if (line.length <= 75)
|
|
20
|
+
return line;
|
|
21
|
+
const parts = [];
|
|
22
|
+
let remaining = line;
|
|
23
|
+
parts.push(remaining.slice(0, 75));
|
|
24
|
+
remaining = remaining.slice(75);
|
|
25
|
+
while (remaining.length > 0) {
|
|
26
|
+
// 74 chars + 1 leading space = 75 octets per continuation line.
|
|
27
|
+
parts.push(" " + remaining.slice(0, 74));
|
|
28
|
+
remaining = remaining.slice(74);
|
|
29
|
+
}
|
|
30
|
+
return parts.join("\r\n");
|
|
31
|
+
}
|
|
32
|
+
// Format a Date as a UTC timestamp: YYYYMMDDTHHMMSSZ (RFC 5545 form 2).
|
|
33
|
+
export function formatUtcTimestamp(d) {
|
|
34
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
35
|
+
return (`${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}` +
|
|
36
|
+
`T${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}Z`);
|
|
37
|
+
}
|
|
38
|
+
// Format a Date as an all-day DATE value: YYYYMMDD (RFC 5545 §3.3.4).
|
|
39
|
+
export function formatDateValue(d) {
|
|
40
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
41
|
+
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`;
|
|
42
|
+
}
|
|
43
|
+
// A pm deadline may be a date-only string (YYYY-MM-DD) or a full ISO datetime.
|
|
44
|
+
// Returns null for anything unparseable so callers can skip the item.
|
|
45
|
+
function parseDeadline(raw) {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
return null;
|
|
49
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
|
|
50
|
+
const d = new Date(dateOnly ? `${trimmed}T00:00:00Z` : trimmed);
|
|
51
|
+
if (Number.isNaN(d.getTime()))
|
|
52
|
+
return null;
|
|
53
|
+
// pm normalizes date-only deadlines to a midnight-UTC ISO timestamp
|
|
54
|
+
// (e.g. "2026-06-10T00:00:00.000Z"). Treat any exact-midnight-UTC value as
|
|
55
|
+
// an all-day event, which renders far better in calendar clients than a
|
|
56
|
+
// zero-length event at 00:00.
|
|
57
|
+
const isMidnightUtc = d.getUTCHours() === 0 &&
|
|
58
|
+
d.getUTCMinutes() === 0 &&
|
|
59
|
+
d.getUTCSeconds() === 0 &&
|
|
60
|
+
d.getUTCMilliseconds() === 0;
|
|
61
|
+
return { date: d, allDay: dateOnly || isMidnightUtc };
|
|
62
|
+
}
|
|
63
|
+
// Build a single VEVENT for a pm item with a deadline. Returns null when the
|
|
64
|
+
// item has no usable deadline.
|
|
65
|
+
export function itemToVevent(item, opts) {
|
|
66
|
+
if (!item.deadline)
|
|
67
|
+
return null;
|
|
68
|
+
const parsed = parseDeadline(item.deadline);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
return null;
|
|
71
|
+
const lines = ["BEGIN:VEVENT"];
|
|
72
|
+
lines.push(`UID:${item.id}@${opts.uidDomain}`);
|
|
73
|
+
lines.push(`DTSTAMP:${opts.dtstamp}`);
|
|
74
|
+
if (parsed.allDay) {
|
|
75
|
+
// All-day event: DTSTART/DTEND use VALUE=DATE, DTEND is the next day
|
|
76
|
+
// (exclusive end) per RFC 5545.
|
|
77
|
+
const end = new Date(parsed.date.getTime() + 24 * 60 * 60 * 1000);
|
|
78
|
+
lines.push(`DTSTART;VALUE=DATE:${formatDateValue(parsed.date)}`);
|
|
79
|
+
lines.push(`DTEND;VALUE=DATE:${formatDateValue(end)}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
lines.push(`DTSTART:${formatUtcTimestamp(parsed.date)}`);
|
|
83
|
+
lines.push(`DTEND:${formatUtcTimestamp(parsed.date)}`);
|
|
84
|
+
}
|
|
85
|
+
const summaryPrefix = item.type ? `[${item.type}] ` : "";
|
|
86
|
+
lines.push(`SUMMARY:${icsEscapeText(summaryPrefix + (item.title ?? item.id))}`);
|
|
87
|
+
const descParts = [`pm item ${item.id}`];
|
|
88
|
+
if (item.status)
|
|
89
|
+
descParts.push(`Status: ${item.status}`);
|
|
90
|
+
if (item.assignee)
|
|
91
|
+
descParts.push(`Assignee: ${item.assignee}`);
|
|
92
|
+
if (item.priority !== undefined && item.priority !== null)
|
|
93
|
+
descParts.push(`Priority: P${item.priority}`);
|
|
94
|
+
if (item.tags && item.tags.length)
|
|
95
|
+
descParts.push(`Tags: ${item.tags.join(", ")}`);
|
|
96
|
+
// Join with real newlines; icsEscapeText turns them into the RFC 5545 "\n"
|
|
97
|
+
// escape sequence (escaping here directly would double-escape).
|
|
98
|
+
lines.push(`DESCRIPTION:${icsEscapeText(descParts.join("\n"))}`);
|
|
99
|
+
// Map pm priority (0 highest .. 4 lowest) onto the iCal 1..9 scale where 1
|
|
100
|
+
// is highest. Leave unset when no priority is present.
|
|
101
|
+
if (typeof item.priority === "number" && item.priority >= 0 && item.priority <= 4) {
|
|
102
|
+
const icalPriority = Math.min(9, Math.max(1, item.priority * 2 + 1));
|
|
103
|
+
lines.push(`PRIORITY:${icalPriority}`);
|
|
104
|
+
}
|
|
105
|
+
if (item.tags && item.tags.length) {
|
|
106
|
+
lines.push(`CATEGORIES:${item.tags.map(icsEscapeText).join(",")}`);
|
|
107
|
+
}
|
|
108
|
+
// Closed/canceled items are recorded as CANCELLED so subscribers can hide
|
|
109
|
+
// them; everything else is CONFIRMED.
|
|
110
|
+
const cancelled = item.status === "closed" || item.status === "canceled";
|
|
111
|
+
lines.push(`STATUS:${cancelled ? "CANCELLED" : "CONFIRMED"}`);
|
|
112
|
+
lines.push("END:VEVENT");
|
|
113
|
+
return lines.map(foldLine).join("\r\n");
|
|
114
|
+
}
|
|
115
|
+
// Build a complete VCALENDAR document from pm items. Items without a usable
|
|
116
|
+
// deadline are skipped. Output uses CRLF line endings per RFC 5545 §3.1.
|
|
117
|
+
export function buildIcsCalendar(items, opts = {}) {
|
|
118
|
+
const now = opts.now ?? new Date();
|
|
119
|
+
const dtstamp = formatUtcTimestamp(now);
|
|
120
|
+
const uidDomain = (opts.uidDomain ?? "pm-web").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
121
|
+
const calName = opts.calendarName ?? "pm-web";
|
|
122
|
+
const head = [
|
|
123
|
+
"BEGIN:VCALENDAR",
|
|
124
|
+
"VERSION:2.0",
|
|
125
|
+
"PRODID:-//pm-web//pm-cli calendar//EN",
|
|
126
|
+
"CALSCALE:GREGORIAN",
|
|
127
|
+
"METHOD:PUBLISH",
|
|
128
|
+
`X-WR-CALNAME:${icsEscapeText(calName)}`,
|
|
129
|
+
`NAME:${icsEscapeText(calName)}`,
|
|
130
|
+
];
|
|
131
|
+
const events = [];
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
const vevent = itemToVevent(item, { uidDomain, dtstamp });
|
|
134
|
+
if (vevent)
|
|
135
|
+
events.push(vevent);
|
|
136
|
+
}
|
|
137
|
+
const all = [...head.map(foldLine), ...events, "END:VCALENDAR"];
|
|
138
|
+
// Trailing CRLF is conventional and accepted by all major parsers.
|
|
139
|
+
return all.join("\r\n") + "\r\n";
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=ical.js.map
|
package/dist/ical.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ical.js","sourceRoot":"","sources":["../src/ical.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/pm.js
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
@@ -822,6 +823,48 @@ router.get("/calendar", async (req, res) => {
|
|
|
822
823
|
});
|
|
823
824
|
res.json(result.ok ? (result.parsed || {}) : { events: [] });
|
|
824
825
|
});
|
|
826
|
+
// GET /api/projects/:projectId/pm/calendar.ics
|
|
827
|
+
// RFC 5545 iCalendar feed of item deadlines, for subscribing in Google
|
|
828
|
+
// Calendar / Outlook / Apple Calendar. Reuses the same list-all read as the
|
|
829
|
+
// calendar view. Auth works via the usual token (header/cookie) or a
|
|
830
|
+
// `?token=` query param, since calendar clients cannot send cookies.
|
|
831
|
+
router.get("/calendar.ics", async (req, res) => {
|
|
832
|
+
const project = await verifyProject(req.user.userId, req.params["projectId"]);
|
|
833
|
+
if (!project) {
|
|
834
|
+
res.status(404).json({ error: "Project not found" });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const listed = runPm({
|
|
838
|
+
args: ["list-all", "--json"],
|
|
839
|
+
userId: project.ownerUserId,
|
|
840
|
+
slug: project.slug,
|
|
841
|
+
jsonOutput: true,
|
|
842
|
+
});
|
|
843
|
+
if (!listed.ok) {
|
|
844
|
+
res.status(502).json({ error: listed.stderr || "Failed to load items" });
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const items = itemsFromListAll(listed.parsed)
|
|
848
|
+
.filter((i) => Boolean(i.deadline))
|
|
849
|
+
.map((i) => ({
|
|
850
|
+
id: i.id,
|
|
851
|
+
title: i.title,
|
|
852
|
+
type: i.type,
|
|
853
|
+
status: i.status,
|
|
854
|
+
priority: i.priority,
|
|
855
|
+
deadline: i.deadline,
|
|
856
|
+
assignee: i.assignee,
|
|
857
|
+
tags: i.tags,
|
|
858
|
+
}));
|
|
859
|
+
const ics = buildIcsCalendar(items, {
|
|
860
|
+
calendarName: `pm · ${project.slug}`,
|
|
861
|
+
uidDomain: `${project.slug}.pm-web`,
|
|
862
|
+
});
|
|
863
|
+
res.setHeader("Content-Type", "text/calendar; charset=utf-8");
|
|
864
|
+
res.setHeader("Content-Disposition", `attachment; filename="pm-${project.slug}.ics"`);
|
|
865
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
866
|
+
res.send(ics);
|
|
867
|
+
});
|
|
825
868
|
// GET /api/projects/:projectId/pm/health
|
|
826
869
|
router.get("/health", async (req, res) => {
|
|
827
870
|
const project = await verifyProject(req.user.userId, req.params["projectId"]);
|