@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +26 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +38 -2
  5. package/dist/app.js.map +1 -1
  6. package/dist/auth.d.ts +2 -1
  7. package/dist/auth.js +10 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/db.js +17 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/ical.js.map +1 -1
  12. package/dist/index.d.ts +1 -22
  13. package/dist/index.js +2 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/oidc.d.ts +89 -0
  16. package/dist/oidc.js +305 -0
  17. package/dist/oidc.js.map +1 -0
  18. package/dist/routes/auth.js +3 -15
  19. package/dist/routes/auth.js.map +1 -1
  20. package/dist/routes/github.js +3 -3
  21. package/dist/routes/github.js.map +1 -1
  22. package/dist/routes/oidc.d.ts +4 -0
  23. package/dist/routes/oidc.js +147 -0
  24. package/dist/routes/oidc.js.map +1 -0
  25. package/dist/routes/pm.js +121 -107
  26. package/dist/routes/pm.js.map +1 -1
  27. package/dist/routes/projects.js +9 -2
  28. package/dist/routes/projects.js.map +1 -1
  29. package/dist/server.js +7 -2
  30. package/dist/server.js.map +1 -1
  31. package/dist/services/pm-runner.d.ts +11 -3
  32. package/dist/services/pm-runner.js +167 -54
  33. package/dist/services/pm-runner.js.map +1 -1
  34. package/dist/services/realtime-bus.d.ts +1 -0
  35. package/dist/services/realtime-bus.js +148 -0
  36. package/dist/services/realtime-bus.js.map +1 -0
  37. package/dist/services/sse.d.ts +3 -1
  38. package/dist/services/sse.js +83 -48
  39. package/dist/services/sse.js.map +1 -1
  40. package/manifest.json +1 -1
  41. package/package.json +11 -11
  42. package/public/cookie-consent.js +54 -39
  43. package/public/cookie-settings.html +7 -7
  44. package/public/index.html +2 -0
  45. package/public/legal-notice.html +10 -12
  46. package/public/privacy-policy.html +14 -24
  47. package/public/src/app.js +4 -4
  48. package/public/src/app.js.map +1 -1
  49. package/public/src/app.ts +4 -4
  50. package/public/src/components/toast.js.map +1 -1
  51. package/public/src/cookie-consent.ts +68 -0
  52. package/public/src/sw.ts +443 -0
  53. package/public/src/views/auth.js +22 -0
  54. package/public/src/views/auth.js.map +1 -1
  55. package/public/src/views/auth.ts +20 -0
  56. package/public/src/views/graph.js.map +1 -1
  57. package/public/styles.css +2 -0
  58. package/public/sw.js +320 -256
  59. package/public/terms.html +7 -9
  60. package/public/tsconfig.json +1 -1
  61. package/public/tsconfig.scripts.json +18 -0
  62. package/public/tsconfig.sw.json +18 -0
  63. package/sql/schema.sql +16 -0
@@ -1,23 +1,81 @@
1
- const clients = [];
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
- clients.push(client);
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
- broadcastPresence(client.projectId);
59
+ schedulePresence(client.projectId);
8
60
  // Return unsubscribe function
9
61
  return () => {
10
- const idx = clients.indexOf(client);
11
- if (idx !== -1)
12
- clients.splice(idx, 1);
62
+ removeClient(client);
13
63
  // Broadcast updated presence after disconnect
14
- broadcastPresence(client.projectId);
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 recipients = clients.filter((c) => c.projectId === projectId);
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 projectClients = clients.filter((c) => c.projectId === projectId);
31
- // Deduplicate by userId keep most recent connection per user
32
- const byUser = new Map();
33
- for (const c of projectClients) {
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 projectClients) {
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 = clients.find((c) => c.id === clientId);
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
- broadcastPresence(client.projectId);
106
+ schedulePresence(client.projectId);
107
+ return true;
60
108
  }
109
+ return false;
61
110
  }
62
111
  export function getProjectPresence(projectId) {
63
- const projectClients = clients.filter((c) => c.projectId === projectId);
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 clients.length;
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 (let i = clients.length - 1; i >= 0; i--) {
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
- broadcastPresence(projectId);
143
+ schedulePresence(projectId);
109
144
  }
110
145
  }
111
146
  //# sourceMappingURL=sse.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/services/sse.ts"],"names":[],"mappings":"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"}
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.10",
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.10",
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 && npx tsc --noEmit",
24
- "build:frontend": "cd public && npx tsc",
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.1.0",
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": "^25.9.2",
54
+ "@types/node": "^26.1.1",
54
55
  "@types/pg": "^8.11.11",
55
56
  "@types/uuid": "^10.0.0",
56
- "@unbrained/pm-cli": "^2026.7.5",
57
- "pm-changelog": "^2026.6.30",
58
- "tsx": "^4.22.4",
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",
@@ -1,43 +1,58 @@
1
- (function () {
2
- var storageKey = "pm_cookie_preferences_v1";
3
- var banner = document.getElementById("cookie-consent");
4
- var links = document.querySelectorAll("[data-cookie-settings]");
5
-
6
- function save(choice) {
7
- try {
8
- localStorage.setItem(storageKey, JSON.stringify({ necessary: true, optional: false, choice: choice, savedAt: new Date().toISOString() }));
9
- } catch (_) {
10
- /* ignore unavailable storage */
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
- function hasChoice() {
15
- try {
16
- return Boolean(localStorage.getItem(storageKey));
17
- } catch (_) {
18
- return false;
29
+ function hasChoice() {
30
+ try {
31
+ return Boolean(localStorage.getItem(STORAGE_KEY));
32
+ }
33
+ catch {
34
+ return false;
35
+ }
19
36
  }
20
- }
21
-
22
- function show(event) {
23
- if (event) event.preventDefault();
24
- if (banner) banner.hidden = false;
25
- }
26
-
27
- function hide() {
28
- if (banner) banner.hidden = true;
29
- }
30
-
31
- links.forEach(function (link) {
32
- link.addEventListener("click", show);
33
- });
34
-
35
- document.querySelectorAll("[data-cookie-accept], [data-cookie-decline]").forEach(function (button) {
36
- button.addEventListener("click", function () {
37
- save(button.hasAttribute("data-cookie-accept") ? "acknowledged" : "necessary");
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="de" data-theme="auto">
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="Cookie- und Speicherinformationen fuer pm-web.">
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">Legal</p>
14
+ <p class="legal-kicker">Operator template — verify the deployed application</p>
15
15
  <h1>Cookie Settings</h1>
16
- <p class="legal-updated">Stand: 26. Juni 2026</p>
17
- <section class="legal-section"><h2>Aktuelle Einstellung</h2><p>pm-web verwendet keine optionalen Tracking- oder Marketing-Cookies. Technisch notwendige Speicherung kann fuer Anmeldung, Sicherheit, Darstellung und Consent-Status erforderlich sein.</p><button type="button" class="btn btn-primary" data-cookie-settings>Open Cookie Settings</button></section>
18
- <section class="legal-section"><h2>Notwendige Speicherung</h2><p><code>pm_token</code> speichert die angemeldete Sitzung als HTTP-only Cookie. Lokale Einstellungen wie Theme, PWA-Installationshinweise und Cookie-Status werden im Browser gespeichert.</p></section>
19
- <section class="legal-section"><h2>Optionale Cookies</h2><p>Derzeit werden keine optionalen Cookies gesetzt. Sollten Analyse- oder Marketingdienste spaeter aktiviert werden, werden sie erst nach ausdruecklicher Einwilligung geladen.</p></section>
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
 
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="de" data-theme="auto">
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><a href="https://pm-cli.unbrained.dev/legal-notice">pm-cli Legal Notice</a></div></nav>
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">Legal</p>
14
+ <p class="legal-kicker">Operator template — not a legal notice</p>
15
15
  <h1>Legal Notice</h1>
16
- <p class="legal-updated">Stand: 26. Juni 2026</p>
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>Angaben gemaess ECG und MedienG</h2>
19
- <p><strong>Stefan Preu</strong><br>Steinbruch 5<br>3250 Wieselburg<br>Oesterreich</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>Online-Angebot</h2>
25
- <p>Dieses Impressum gilt fuer <a href="https://pm-web.unbrained.dev/">pm-web.unbrained.dev</a>. pm-web ist eine webbasierte Oberflaeche fuer pm-cli.</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>Haftung und externe Links</h2>
29
- <p>Die Inhalte werden sorgfaeltig gepflegt. Fuer externe Links wird keine Verantwortung fuer fremde Inhalte uebernommen; fuer diese sind ausschliesslich deren Betreiber verantwortlich.</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="de" data-theme="auto">
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><a href="https://pm-cli.unbrained.dev/privacy-policy">pm-cli Privacy Policy</a></div></nav>
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">Legal</p>
14
+ <p class="legal-kicker">Operator template — not a privacy notice</p>
15
15
  <h1>Privacy Policy</h1>
16
- <p class="legal-updated">Stand: 26. Juni 2026</p>
16
+ <p class="legal-updated">A package cannot describe an unknown operator's actual processing.</p>
17
17
  <section class="legal-section">
18
- <h2>Verantwortlicher</h2>
19
- <p>Stefan Preu, Steinbruch 5, 3250 Wieselburg, Oesterreich, <a href="mailto:stefan@preu.at">stefan@preu.at</a>.</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>Verarbeitete Daten</h2>
23
- <p>Beim Besuch werden technisch notwendige Serverdaten verarbeitet, insbesondere IP-Adresse, Zeitpunkt, angefragte URL, HTTP-Status, User-Agent und Referrer.</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>Zwecke und Rechtsgrundlagen</h2>
28
- <p>Die Verarbeitung erfolgt zur Bereitstellung der Anwendung, Authentifizierung, Projektverwaltung, Zusammenarbeit, Sicherheit, Missbrauchsschutz und Fehleranalyse. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, lit. f und, soweit einschlaegig, lit. c DSGVO.</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>Cookies und lokale Speicherung</h2>
32
- <p>pm-web verwendet ein technisch notwendiges HTTP-only Authentifizierungs-Cookie (<code>pm_token</code>). Lokale Browser-Speicherung kann fuer Theme, Installationshinweise und Cookie-Status genutzt werden. Es werden keine optionalen Tracking- oder Marketing-Cookies gesetzt.</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>Drittanbieter</h2>
36
- <p>Schriftarten werden lokal beziehungsweise ueber Systemschriftarten dargestellt. GitHub-Funktionen werden nur genutzt, wenn Nutzer diese aktiv verwenden oder entsprechende Links oeffnen.</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?dn=${displayName}&view=${currentView}`;
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();