@unbrained/pm-web 2026.7.10 → 2026.7.13
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 +13 -0
- package/README.md +26 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +38 -2
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +10 -0
- package/dist/auth.js.map +1 -1
- package/dist/db.js +17 -1
- package/dist/db.js.map +1 -1
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +1 -22
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +89 -0
- package/dist/oidc.js +305 -0
- package/dist/oidc.js.map +1 -0
- package/dist/routes/auth.js +3 -15
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/github.js +3 -3
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +4 -0
- package/dist/routes/oidc.js +147 -0
- package/dist/routes/oidc.js.map +1 -0
- package/dist/routes/pm.js +121 -107
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +9 -2
- package/dist/routes/projects.js.map +1 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/dist/services/pm-runner.d.ts +11 -3
- package/dist/services/pm-runner.js +167 -54
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +1 -0
- package/dist/services/realtime-bus.js +148 -0
- package/dist/services/realtime-bus.js.map +1 -0
- package/dist/services/sse.d.ts +3 -1
- package/dist/services/sse.js +83 -48
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +11 -11
- package/public/cookie-consent.js +54 -39
- package/public/cookie-settings.html +7 -7
- package/public/index.html +2 -0
- package/public/legal-notice.html +10 -12
- package/public/privacy-policy.html +14 -24
- package/public/src/app.js +4 -4
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +4 -4
- package/public/src/components/toast.js.map +1 -1
- package/public/src/cookie-consent.ts +68 -0
- package/public/src/sw.ts +443 -0
- package/public/src/views/auth.js +22 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +20 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/styles.css +2 -0
- package/public/sw.js +320 -256
- package/public/terms.html +7 -9
- package/public/tsconfig.json +1 -1
- package/public/tsconfig.scripts.json +18 -0
- package/public/tsconfig.sw.json +18 -0
- package/sql/schema.sql +16 -0
package/dist/services/sse.js
CHANGED
|
@@ -1,23 +1,81 @@
|
|
|
1
|
-
|
|
1
|
+
// Clients are indexed two ways so that per-event work scales with the number of
|
|
2
|
+
// clients on the *affected project*, not the total number of connected clients.
|
|
3
|
+
// This is what lets a single project sustain many concurrent viewers without an
|
|
4
|
+
// O(total-clients) scan on every event, presence update, and disconnect.
|
|
5
|
+
const byId = new Map();
|
|
6
|
+
const byProject = new Map();
|
|
7
|
+
const presenceTimers = new Map();
|
|
8
|
+
let projectEventPublisher = null;
|
|
9
|
+
export function configureProjectEventPublisher(publisher) {
|
|
10
|
+
projectEventPublisher = publisher;
|
|
11
|
+
}
|
|
12
|
+
function removeClient(client) {
|
|
13
|
+
if (byId.get(client.id) === client)
|
|
14
|
+
byId.delete(client.id);
|
|
15
|
+
const set = byProject.get(client.projectId);
|
|
16
|
+
if (set) {
|
|
17
|
+
set.delete(client);
|
|
18
|
+
if (set.size === 0)
|
|
19
|
+
byProject.delete(client.projectId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function presenceUsers(set) {
|
|
23
|
+
if (!set || set.size === 0)
|
|
24
|
+
return [];
|
|
25
|
+
// Deduplicate by userId — keep most recent connection per user
|
|
26
|
+
const byUser = new Map();
|
|
27
|
+
for (const c of set) {
|
|
28
|
+
const existing = byUser.get(c.userId);
|
|
29
|
+
if (!existing || c.connectedAt > existing.connectedAt)
|
|
30
|
+
byUser.set(c.userId, c);
|
|
31
|
+
}
|
|
32
|
+
return [...byUser.values()].map((c) => ({
|
|
33
|
+
userId: c.userId,
|
|
34
|
+
displayName: c.displayName,
|
|
35
|
+
currentView: c.currentView,
|
|
36
|
+
connectedAt: c.connectedAt.toISOString(),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function schedulePresence(projectId) {
|
|
40
|
+
const active = presenceTimers.get(projectId);
|
|
41
|
+
if (active)
|
|
42
|
+
clearTimeout(active);
|
|
43
|
+
presenceTimers.set(projectId, setTimeout(() => {
|
|
44
|
+
presenceTimers.delete(projectId);
|
|
45
|
+
broadcastPresence(projectId);
|
|
46
|
+
}, 75));
|
|
47
|
+
}
|
|
2
48
|
export function addSSEClient(client) {
|
|
3
|
-
|
|
49
|
+
byId.set(client.id, client);
|
|
50
|
+
let set = byProject.get(client.projectId);
|
|
51
|
+
if (!set) {
|
|
52
|
+
set = new Set();
|
|
53
|
+
byProject.set(client.projectId, set);
|
|
54
|
+
}
|
|
55
|
+
set.add(client);
|
|
4
56
|
// Send initial connection confirmation
|
|
5
57
|
client.res.write(`event: connected\ndata: ${JSON.stringify({ ok: true, clientId: client.id })}\n\n`);
|
|
6
58
|
// Broadcast presence update to all project viewers
|
|
7
|
-
|
|
59
|
+
schedulePresence(client.projectId);
|
|
8
60
|
// Return unsubscribe function
|
|
9
61
|
return () => {
|
|
10
|
-
|
|
11
|
-
if (idx !== -1)
|
|
12
|
-
clients.splice(idx, 1);
|
|
62
|
+
removeClient(client);
|
|
13
63
|
// Broadcast updated presence after disconnect
|
|
14
|
-
|
|
64
|
+
schedulePresence(client.projectId);
|
|
15
65
|
};
|
|
16
66
|
}
|
|
17
67
|
export function broadcastProjectEvent(projectId, event) {
|
|
68
|
+
deliverProjectEvent(projectId, event);
|
|
69
|
+
if (projectEventPublisher) {
|
|
70
|
+
void projectEventPublisher(projectId, event).catch(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function deliverProjectEvent(projectId, event) {
|
|
74
|
+
const set = byProject.get(projectId);
|
|
75
|
+
if (!set || set.size === 0)
|
|
76
|
+
return;
|
|
18
77
|
const payload = `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
|
19
|
-
const
|
|
20
|
-
for (const client of recipients) {
|
|
78
|
+
for (const client of set) {
|
|
21
79
|
try {
|
|
22
80
|
client.res.write(payload);
|
|
23
81
|
}
|
|
@@ -27,23 +85,12 @@ export function broadcastProjectEvent(projectId, event) {
|
|
|
27
85
|
}
|
|
28
86
|
}
|
|
29
87
|
export function broadcastPresence(projectId) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const existing = byUser.get(c.userId);
|
|
35
|
-
if (!existing || c.connectedAt > existing.connectedAt) {
|
|
36
|
-
byUser.set(c.userId, c);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const users = [...byUser.values()].map((c) => ({
|
|
40
|
-
userId: c.userId,
|
|
41
|
-
displayName: c.displayName,
|
|
42
|
-
currentView: c.currentView,
|
|
43
|
-
connectedAt: c.connectedAt.toISOString(),
|
|
44
|
-
}));
|
|
88
|
+
const set = byProject.get(projectId);
|
|
89
|
+
if (!set || set.size === 0)
|
|
90
|
+
return;
|
|
91
|
+
const users = presenceUsers(set);
|
|
45
92
|
const payload = `event: presence\ndata: ${JSON.stringify({ users })}\n\n`;
|
|
46
|
-
for (const client of
|
|
93
|
+
for (const client of set) {
|
|
47
94
|
try {
|
|
48
95
|
client.res.write(payload);
|
|
49
96
|
}
|
|
@@ -52,28 +99,17 @@ export function broadcastPresence(projectId) {
|
|
|
52
99
|
}
|
|
53
100
|
}
|
|
54
101
|
}
|
|
55
|
-
export function updateClientView(clientId, currentView) {
|
|
56
|
-
const client =
|
|
57
|
-
if (client) {
|
|
102
|
+
export function updateClientView(clientId, userId, projectId, currentView) {
|
|
103
|
+
const client = byId.get(clientId);
|
|
104
|
+
if (client && client.userId === userId && client.projectId === projectId) {
|
|
58
105
|
client.currentView = currentView;
|
|
59
|
-
|
|
106
|
+
schedulePresence(client.projectId);
|
|
107
|
+
return true;
|
|
60
108
|
}
|
|
109
|
+
return false;
|
|
61
110
|
}
|
|
62
111
|
export function getProjectPresence(projectId) {
|
|
63
|
-
|
|
64
|
-
const byUser = new Map();
|
|
65
|
-
for (const c of projectClients) {
|
|
66
|
-
const existing = byUser.get(c.userId);
|
|
67
|
-
if (!existing || c.connectedAt > existing.connectedAt) {
|
|
68
|
-
byUser.set(c.userId, c);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return [...byUser.values()].map((c) => ({
|
|
72
|
-
userId: c.userId,
|
|
73
|
-
displayName: c.displayName,
|
|
74
|
-
currentView: c.currentView,
|
|
75
|
-
connectedAt: c.connectedAt.toISOString(),
|
|
76
|
-
}));
|
|
112
|
+
return presenceUsers(byProject.get(projectId));
|
|
77
113
|
}
|
|
78
114
|
export function setupSSEHeaders(res) {
|
|
79
115
|
res.writeHead(200, {
|
|
@@ -84,13 +120,12 @@ export function setupSSEHeaders(res) {
|
|
|
84
120
|
});
|
|
85
121
|
}
|
|
86
122
|
export function getSSEClientCount() {
|
|
87
|
-
return
|
|
123
|
+
return byId.size;
|
|
88
124
|
}
|
|
89
125
|
export function cleanupStaleClients() {
|
|
90
126
|
const now = Date.now();
|
|
91
127
|
const staleProjectIds = new Set();
|
|
92
|
-
for (
|
|
93
|
-
const client = clients[i];
|
|
128
|
+
for (const client of [...byId.values()]) {
|
|
94
129
|
// If client connection has been open > 12 hours, close it
|
|
95
130
|
if (now - client.connectedAt.getTime() > 12 * 60 * 60 * 1000) {
|
|
96
131
|
try {
|
|
@@ -99,13 +134,13 @@ export function cleanupStaleClients() {
|
|
|
99
134
|
catch {
|
|
100
135
|
// Already closed
|
|
101
136
|
}
|
|
137
|
+
removeClient(client);
|
|
102
138
|
staleProjectIds.add(client.projectId);
|
|
103
|
-
clients.splice(i, 1);
|
|
104
139
|
}
|
|
105
140
|
}
|
|
106
141
|
// Broadcast updated presence for affected projects
|
|
107
142
|
for (const projectId of staleProjectIds) {
|
|
108
|
-
|
|
143
|
+
schedulePresence(projectId);
|
|
109
144
|
}
|
|
110
145
|
}
|
|
111
146
|
//# sourceMappingURL=sse.js.map
|
package/dist/services/sse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/services/sse.ts"],"names":[],"mappings":"AAmBA,MAAM,
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/services/sse.ts"],"names":[],"mappings":"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"}
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pm-web",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.13",
|
|
4
4
|
"description": "Full web UI for pm-cli — browse, create, update, search and manage pm projects in the browser. Self-hosted via Docker or Node.js.",
|
|
5
5
|
"author": "@unbraind",
|
|
6
6
|
"entry": "./dist/index.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unbrained/pm-web",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.13",
|
|
4
4
|
"description": "Full web UI for pm-cli — browse, create, update, search and manage pm projects in the browser. Self-hosted via Docker or Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"typecheck": "npm run typecheck:graph && npm run typecheck:server && npm run typecheck:frontend",
|
|
21
21
|
"typecheck:graph": "cd extensions/pm-graph && npm run typecheck",
|
|
22
22
|
"typecheck:server": "tsc --noEmit",
|
|
23
|
-
"typecheck:frontend": "cd public &&
|
|
24
|
-
"build:frontend": "cd public &&
|
|
23
|
+
"typecheck:frontend": "cd public && tsc --noEmit && tsc -p tsconfig.sw.json --noEmit && tsc -p tsconfig.scripts.json --noEmit",
|
|
24
|
+
"build:frontend": "cd public && tsc && tsc -p tsconfig.sw.json && tsc -p tsconfig.scripts.json",
|
|
25
25
|
"build:all": "npm run build",
|
|
26
26
|
"check": "npm run typecheck",
|
|
27
|
-
"audit:prod": "npm audit --omit=dev",
|
|
27
|
+
"audit:prod": "env -u npm_config_allow_scripts npm audit --omit=dev --ignore-scripts",
|
|
28
28
|
"pack:dry-run": "npm pack --dry-run",
|
|
29
29
|
"changelog": "pm-changelog --pm-root .agents/pm --mode prepend --output CHANGELOG.md --release-version-from-package --since-previous-tag --until-release-tag --item-url-base https://github.com/unbraind/pm-web/blob/main/.agents/pm",
|
|
30
30
|
"changelog:full": "pm-changelog --pm-root .agents/pm --mode replace --output CHANGELOG.md --all-release-tags --release-version-from-package --item-url-base https://github.com/unbraind/pm-web/blob/main/.agents/pm",
|
|
@@ -37,26 +37,26 @@
|
|
|
37
37
|
"prepublishOnly": "npm run release:check"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@unbrained/pm-cli": "^2026.7.12",
|
|
40
41
|
"bcryptjs": "^3.0.3",
|
|
41
42
|
"cookie-parser": "^1.4.7",
|
|
42
43
|
"express": "^5.2.1",
|
|
43
44
|
"jsonwebtoken": "^9.0.2",
|
|
44
|
-
"neo4j-driver": "^6.
|
|
45
|
+
"neo4j-driver": "^6.2.0",
|
|
46
|
+
"openid-client": "6.8.4",
|
|
45
47
|
"pg": "^8.21.0",
|
|
46
48
|
"uuid": "^14.0.0"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"@types/bcryptjs": "^2.4.6",
|
|
50
51
|
"@types/cookie-parser": "^1.4.8",
|
|
51
52
|
"@types/express": "^5.0.6",
|
|
52
53
|
"@types/jsonwebtoken": "^9.0.9",
|
|
53
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^26.1.1",
|
|
54
55
|
"@types/pg": "^8.11.11",
|
|
55
56
|
"@types/uuid": "^10.0.0",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"typescript": "^6.0.3"
|
|
57
|
+
"pm-changelog": "^2026.7.12",
|
|
58
|
+
"tsx": "^4.23.0",
|
|
59
|
+
"typescript": "^7.0.2"
|
|
60
60
|
},
|
|
61
61
|
"repository": {
|
|
62
62
|
"type": "git",
|
package/public/cookie-consent.js
CHANGED
|
@@ -1,43 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════
|
|
3
|
+
// COOKIE CONSENT BANNER — pm-web
|
|
4
|
+
//
|
|
5
|
+
// This is the TypeScript source for /cookie-consent.js. It is compiled
|
|
6
|
+
// with the `DOM` lib (see public/tsconfig.scripts.json) and emitted as
|
|
7
|
+
// a classic browser script (no import/export) so it can be loaded via
|
|
8
|
+
// `<script src="/cookie-consent.js">` without a module loader. The IIFE
|
|
9
|
+
// keeps the global scope clean.
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════
|
|
11
|
+
(() => {
|
|
12
|
+
const STORAGE_KEY = 'pm_cookie_preferences_v1';
|
|
13
|
+
const banner = document.getElementById('cookie-consent');
|
|
14
|
+
const links = document.querySelectorAll('[data-cookie-settings]');
|
|
15
|
+
function save(choice) {
|
|
16
|
+
const preferences = {
|
|
17
|
+
necessary: true,
|
|
18
|
+
optional: false,
|
|
19
|
+
choice,
|
|
20
|
+
savedAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* ignore unavailable storage */
|
|
27
|
+
}
|
|
11
28
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
function hasChoice() {
|
|
30
|
+
try {
|
|
31
|
+
return Boolean(localStorage.getItem(STORAGE_KEY));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
19
36
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
hide();
|
|
37
|
+
function show(event) {
|
|
38
|
+
if (event)
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
if (banner)
|
|
41
|
+
banner.hidden = false;
|
|
42
|
+
}
|
|
43
|
+
function hide() {
|
|
44
|
+
if (banner)
|
|
45
|
+
banner.hidden = true;
|
|
46
|
+
}
|
|
47
|
+
links.forEach((link) => {
|
|
48
|
+
link.addEventListener('click', show);
|
|
49
|
+
});
|
|
50
|
+
document.querySelectorAll('[data-cookie-accept], [data-cookie-decline]').forEach((button) => {
|
|
51
|
+
button.addEventListener('click', () => {
|
|
52
|
+
save(button.hasAttribute('data-cookie-accept') ? 'acknowledged' : 'necessary');
|
|
53
|
+
hide();
|
|
54
|
+
});
|
|
39
55
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!hasChoice()) show();
|
|
56
|
+
if (!hasChoice())
|
|
57
|
+
show();
|
|
43
58
|
})();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en" data-theme="auto" data-package-legal-template>
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<meta name="description" content="
|
|
6
|
+
<meta name="description" content="Package template for pm-web browser-storage disclosures.">
|
|
7
7
|
<title>Cookie Settings - pm-web</title>
|
|
8
8
|
<link rel="stylesheet" href="/styles.css?v=20">
|
|
9
9
|
<link rel="stylesheet" href="/legal-common.css?v=1">
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
<body class="legal-shell">
|
|
12
12
|
<nav class="legal-nav"><a class="legal-brand" href="/">pm-web</a><div class="legal-links"><a href="/legal-notice">Legal Notice</a><a href="/privacy-policy">Privacy Policy</a><a href="/terms">Terms</a></div></nav>
|
|
13
13
|
<main class="legal-main">
|
|
14
|
-
<p class="legal-kicker">
|
|
14
|
+
<p class="legal-kicker">Operator template — verify the deployed application</p>
|
|
15
15
|
<h1>Cookie Settings</h1>
|
|
16
|
-
<p class="legal-updated">
|
|
17
|
-
<section class="legal-section"><h2>
|
|
18
|
-
<section class="legal-section"><h2>
|
|
19
|
-
<section class="legal-section"><h2>
|
|
16
|
+
<p class="legal-updated">This page describes package capabilities, not an operator's full deployment.</p>
|
|
17
|
+
<section class="legal-section"><h2>Package defaults</h2><p>pm-web can use an HTTP-only <code>pm_token</code> session cookie, short-lived OIDC flow state when configured, and local browser preferences such as theme, PWA prompts, and this banner choice.</p><button type="button" class="btn btn-primary" data-cookie-settings>Open Cookie Settings</button></section>
|
|
18
|
+
<section class="legal-section"><h2>Operator verification required</h2><p>Reverse proxies, identity providers, analytics, embeds, support tools, and other deployment components may add cookies or storage. Inventory the running service and replace this page with a reviewed disclosure.</p></section>
|
|
19
|
+
<section class="legal-section"><h2>No package-level consent claim</h2><p>The reusable package cannot decide which storage is necessary, optional, or consent-based for an unknown deployment and jurisdiction.</p></section>
|
|
20
20
|
</main>
|
|
21
21
|
<div class="cookie-consent" id="cookie-consent" hidden><div class="cookie-consent-text"><strong>Cookie Settings</strong><span>We only use technically necessary storage for sign-in, security, and preferences. No optional tracking cookies.</span></div><div class="cookie-consent-actions"><a href="/privacy-policy">Privacy Policy</a><button type="button" class="btn btn-secondary btn-sm" data-cookie-decline>Necessary only</button><button type="button" class="btn btn-primary btn-sm" data-cookie-accept>Got it</button></div></div>
|
|
22
22
|
<script src="/cookie-consent.js?v=1"></script>
|
package/public/index.html
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
<span id="auth-btn-text">Sign In</span>
|
|
59
59
|
</button>
|
|
60
60
|
</form>
|
|
61
|
+
<div id="oidc-divider" class="auth-form-sub" style="margin:18px 0 10px;text-align:center" hidden>or</div>
|
|
62
|
+
<button id="oidc-login" type="button" class="btn btn-secondary btn-full btn-lg" hidden onclick="window.__app.startOidcLogin()">Continue with OpenID Connect</button>
|
|
61
63
|
</div>
|
|
62
64
|
</div>
|
|
63
65
|
|
package/public/legal-notice.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en" data-theme="auto" data-package-legal-template>
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -9,24 +9,22 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/legal-common.css?v=1">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="legal-shell">
|
|
12
|
-
<nav class="legal-nav"><a class="legal-brand" href="/">pm-web</a><div class="legal-links"><a href="/privacy-policy">Privacy Policy</a><a href="/terms">Terms</a><a href="/cookie-settings">Cookies</a
|
|
12
|
+
<nav class="legal-nav"><a class="legal-brand" href="/">pm-web</a><div class="legal-links"><a href="/privacy-policy">Privacy Policy</a><a href="/terms">Terms</a><a href="/cookie-settings">Cookies</a></div></nav>
|
|
13
13
|
<main class="legal-main">
|
|
14
|
-
<p class="legal-kicker">
|
|
14
|
+
<p class="legal-kicker">Operator template — not a legal notice</p>
|
|
15
15
|
<h1>Legal Notice</h1>
|
|
16
|
-
<p class="legal-updated">
|
|
16
|
+
<p class="legal-updated">The pm-web package does not know who operates this deployment.</p>
|
|
17
17
|
<section class="legal-section">
|
|
18
|
-
<h2>
|
|
19
|
-
<p
|
|
20
|
-
<p>E-Mail: <a href="mailto:stefan@preu.at">stefan@preu.at</a></p>
|
|
21
|
-
<p>Verantwortlich fuer den Inhalt dieser Website: Stefan Preu.</p>
|
|
18
|
+
<h2>Operator action required</h2>
|
|
19
|
+
<p>This placeholder is part of a reusable open-source package. It is not valid for a real service and must be replaced with an operator-reviewed notice before public deployment.</p>
|
|
22
20
|
</section>
|
|
23
21
|
<section class="legal-section">
|
|
24
|
-
<h2>
|
|
25
|
-
<p>
|
|
22
|
+
<h2>Information to provide</h2>
|
|
23
|
+
<p>Identify the responsible operator, service address, monitored contact channel, persons responsible for content, applicable registrations, and any jurisdiction-specific disclosures.</p>
|
|
26
24
|
</section>
|
|
27
25
|
<section class="legal-section">
|
|
28
|
-
<h2>
|
|
29
|
-
<p>
|
|
26
|
+
<h2>Deployment</h2>
|
|
27
|
+
<p>Mount a complete private legal-page overlay with <code>PM_WEB_LEGAL_DIR</code>. pm-web rejects incomplete, unreadable, symlinked, or path-escaping overlays.</p>
|
|
30
28
|
</section>
|
|
31
29
|
</main>
|
|
32
30
|
<div class="cookie-consent" id="cookie-consent" hidden><div class="cookie-consent-text"><strong>Cookie Settings</strong><span>We only use technically necessary storage for sign-in, security, and preferences. No optional tracking cookies.</span></div><div class="cookie-consent-actions"><a href="/privacy-policy">Privacy Policy</a><button type="button" class="btn btn-secondary btn-sm" data-cookie-decline>Necessary only</button><button type="button" class="btn btn-primary btn-sm" data-cookie-accept>Got it</button></div></div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en" data-theme="auto" data-package-legal-template>
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -9,40 +9,30 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/legal-common.css?v=1">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="legal-shell">
|
|
12
|
-
<nav class="legal-nav"><a class="legal-brand" href="/">pm-web</a><div class="legal-links"><a href="/legal-notice">Legal Notice</a><a href="/terms">Terms</a><a href="/cookie-settings">Cookies</a
|
|
12
|
+
<nav class="legal-nav"><a class="legal-brand" href="/">pm-web</a><div class="legal-links"><a href="/legal-notice">Legal Notice</a><a href="/terms">Terms</a><a href="/cookie-settings">Cookies</a></div></nav>
|
|
13
13
|
<main class="legal-main">
|
|
14
|
-
<p class="legal-kicker">
|
|
14
|
+
<p class="legal-kicker">Operator template — not a privacy notice</p>
|
|
15
15
|
<h1>Privacy Policy</h1>
|
|
16
|
-
<p class="legal-updated">
|
|
16
|
+
<p class="legal-updated">A package cannot describe an unknown operator's actual processing.</p>
|
|
17
17
|
<section class="legal-section">
|
|
18
|
-
<h2>
|
|
19
|
-
<p>
|
|
18
|
+
<h2>Operator action required</h2>
|
|
19
|
+
<p>Replace this entire page with a reviewed notice for the real deployment. Do not rely on package defaults as legal or compliance advice.</p>
|
|
20
20
|
</section>
|
|
21
21
|
<section class="legal-section">
|
|
22
|
-
<h2>
|
|
23
|
-
<p>
|
|
24
|
-
<p>Bei Registrierung und Nutzung verarbeitet pm-web Konto- und Nutzungsdaten: E-Mail-Adresse, Anzeigename, Passwort-Hash, Projekt- und Gruppendaten, Freigaben, optionale GitHub-Verknuepfungsdaten sowie technische Sitzungsdaten.</p>
|
|
22
|
+
<h2>Inventory the deployment</h2>
|
|
23
|
+
<p>Document the actual operator, contacts, account and project data, authentication methods, collaboration and sharing, browser storage, server and security logs, backups, retention, deletion, exports, and user rights.</p>
|
|
25
24
|
</section>
|
|
26
25
|
<section class="legal-section">
|
|
27
|
-
<h2>
|
|
28
|
-
<p>
|
|
26
|
+
<h2>Enabled integrations</h2>
|
|
27
|
+
<p>Describe only services actually enabled by the operator, including identity providers, Git hosting, databases, graph/search systems, email, proxies, analytics, observability, and support tooling.</p>
|
|
29
28
|
</section>
|
|
30
29
|
<section class="legal-section">
|
|
31
|
-
<h2>
|
|
32
|
-
<p>
|
|
30
|
+
<h2>Legal review</h2>
|
|
31
|
+
<p>Have qualified counsel determine applicable purposes, legal bases, recipients, transfers, retention periods, notices, consent requirements, and supervisory contacts for the deployment.</p>
|
|
33
32
|
</section>
|
|
34
33
|
<section class="legal-section">
|
|
35
|
-
<h2>
|
|
36
|
-
<p>
|
|
37
|
-
</section>
|
|
38
|
-
<section class="legal-section">
|
|
39
|
-
<h2>Speicherdauer</h2>
|
|
40
|
-
<p>Konto- und Projektdaten bleiben gespeichert, bis sie geloescht werden oder gesetzliche Pflichten entgegenstehen. Server- und Sicherheitslogs werden nur so lange gespeichert, wie sie fuer Betrieb, Sicherheit und Fehleranalyse erforderlich sind.</p>
|
|
41
|
-
</section>
|
|
42
|
-
<section class="legal-section">
|
|
43
|
-
<h2>Betroffenenrechte</h2>
|
|
44
|
-
<p>Betroffene Personen haben Rechte auf Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch und Beschwerde bei einer Aufsichtsbehoerde. Anfragen bitte an <a href="mailto:stefan@preu.at">stefan@preu.at</a>.</p>
|
|
45
|
-
<p>Zustaendige oesterreichische Aufsichtsbehoerde: Datenschutzbehoerde, Barichgasse 40-42, 1030 Wien, <a href="https://www.dsb.gv.at/" target="_blank" rel="noopener">dsb.gv.at</a>.</p>
|
|
34
|
+
<h2>Private overlay</h2>
|
|
35
|
+
<p>Provide all four reviewed pages through <code>PM_WEB_LEGAL_DIR</code>; partial overlays fail at startup.</p>
|
|
46
36
|
</section>
|
|
47
37
|
</main>
|
|
48
38
|
<div class="cookie-consent" id="cookie-consent" hidden><div class="cookie-consent-text"><strong>Cookie Settings</strong><span>We only use technically necessary storage for sign-in, security, and preferences. No optional tracking cookies.</span></div><div class="cookie-consent-actions"><a href="/privacy-policy">Privacy Policy</a><button type="button" class="btn btn-secondary btn-sm" data-cookie-decline>Necessary only</button><button type="button" class="btn btn-primary btn-sm" data-cookie-accept>Got it</button></div></div>
|
package/public/src/app.js
CHANGED
|
@@ -39,7 +39,7 @@ import { renderCommentsAuditView } from './views/comments-audit.js';
|
|
|
39
39
|
import { renderConfigView, configAddArrayItem, configRemoveArrayItem, configSaveArray, configSaveSimple, configSaveObject, addSchemaType } from './views/config.js';
|
|
40
40
|
import { renderGuideView } from './views/guide.js';
|
|
41
41
|
import { renderAdminView, setAdminRole, adminSwitchTab, adminDeleteUser, adminDeleteProject, adminDeleteGroup, adminFilterUsers, adminFilterProjects, adminFilterAudit, adminSetPage, adminCreateGroup } from './views/admin.js';
|
|
42
|
-
import { switchAuthTab, submitAuth, logout, showAuth } from './views/auth.js';
|
|
42
|
+
import { switchAuthTab, submitAuth, logout, showAuth, startOidcLogin } from './views/auth.js';
|
|
43
43
|
import { initPlanView, openPlanDetail, openCreatePlanModal, submitCreatePlan, openAddStepModal, submitAddStep, planCompleteStep, planBlockStepPrompt, submitBlockStep, planRemoveStep, planApprove, planMaterializePrompt, submitMaterializePlan, copyPlanAgentBrief, copyPlanNextStepPrompt, planEditPrompt, submitEditPlan, planDeletePrompt } from './views/plan.js';
|
|
44
44
|
import { showModal, hideModal, createModal } from './components/modals.js';
|
|
45
45
|
import { toast } from './components/toast.js';
|
|
@@ -225,6 +225,7 @@ window.__app = {
|
|
|
225
225
|
// Auth
|
|
226
226
|
switchAuthTab,
|
|
227
227
|
submitAuth,
|
|
228
|
+
startOidcLogin,
|
|
228
229
|
logout,
|
|
229
230
|
// Projects
|
|
230
231
|
onProjectSelect,
|
|
@@ -456,10 +457,8 @@ function connectSSE(projectId, attempt = 0) {
|
|
|
456
457
|
disconnectSSE();
|
|
457
458
|
sseCurrentProjectId = projectId;
|
|
458
459
|
setSseStatus(attempt > 0 ? 'reconnecting' : 'disconnected');
|
|
459
|
-
const u = state.user;
|
|
460
|
-
const displayName = encodeURIComponent(u?.display_name || u?.email || '');
|
|
461
460
|
const currentView = encodeURIComponent(state.currentView || 'items');
|
|
462
|
-
const url = `/api/projects/${encodeURIComponent(projectId)}/pm/events?
|
|
461
|
+
const url = `/api/projects/${encodeURIComponent(projectId)}/pm/events?view=${currentView}`;
|
|
463
462
|
try {
|
|
464
463
|
const source = new EventSource(url);
|
|
465
464
|
sseSource = source;
|
|
@@ -578,6 +577,7 @@ function connectSSE(projectId, attempt = 0) {
|
|
|
578
577
|
source.addEventListener('graph-sync-failed', graphSyncFailed);
|
|
579
578
|
source.addEventListener('graph_sync_failed', graphSyncFailed);
|
|
580
579
|
source.addEventListener('update', refreshView);
|
|
580
|
+
source.addEventListener('workspace-changed', refreshGraphData);
|
|
581
581
|
source.onerror = () => {
|
|
582
582
|
setSseStatus('reconnecting');
|
|
583
583
|
source.close();
|