@unbrained/pm-web 2026.7.11 → 2026.7.17-1

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 (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +26 -0
  3. package/dist/app.d.ts +16 -1
  4. package/dist/app.js +53 -3
  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 +172 -108
  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 +27 -3
  32. package/dist/services/pm-runner.js +188 -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 +32 -26
  45. package/public/legal-notice.html +10 -12
  46. package/public/privacy-policy.html +14 -24
  47. package/public/src/app.js +18 -4
  48. package/public/src/app.js.map +1 -1
  49. package/public/src/app.ts +19 -4
  50. package/public/src/components/modals.js +5 -3
  51. package/public/src/components/modals.js.map +1 -1
  52. package/public/src/components/modals.ts +6 -3
  53. package/public/src/components/toast.js.map +1 -1
  54. package/public/src/cookie-consent.ts +68 -0
  55. package/public/src/i18n/de.json +117 -0
  56. package/public/src/i18n/en.json +117 -0
  57. package/public/src/i18n/es.json +117 -0
  58. package/public/src/i18n/zh.json +117 -0
  59. package/public/src/i18n.js +287 -0
  60. package/public/src/i18n.js.map +1 -0
  61. package/public/src/i18n.ts +306 -0
  62. package/public/src/sw.ts +446 -0
  63. package/public/src/views/auth.js +29 -6
  64. package/public/src/views/auth.js.map +1 -1
  65. package/public/src/views/auth.ts +27 -6
  66. package/public/src/views/graph.js.map +1 -1
  67. package/public/src/views/settings.js +73 -48
  68. package/public/src/views/settings.js.map +1 -1
  69. package/public/src/views/settings.ts +77 -48
  70. package/public/styles.css +2 -0
  71. package/public/sw.js +323 -256
  72. package/public/terms.html +7 -9
  73. package/public/tsconfig.json +1 -1
  74. package/public/tsconfig.scripts.json +18 -0
  75. package/public/tsconfig.sw.json +18 -0
  76. 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.11",
3
+ "version": "2026.7.17-1",
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.11",
3
+ "version": "2026.7.17-1",
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.15",
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": "^3.0.0",
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
@@ -10,6 +10,10 @@
10
10
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11
11
  <meta name="apple-mobile-web-app-title" content="pm-web">
12
12
  <title>pm-web — Project Management</title>
13
+ <!-- Set <html lang> from the persisted locale BEFORE first paint so assistive
14
+ tech and search engines report the right language without a flash. The
15
+ full catalog is applied later by the i18n module (public/src/i18n.ts). -->
16
+ <script>try{const S=['en','de','es','zh'];const l=localStorage.getItem('pmLocale');if(S.indexOf(l)>=0){document.documentElement.lang=l;}else{const n=(navigator.language||'').toLowerCase().split('-')[0];if(S.indexOf(n)>=0){document.documentElement.lang=n;}}}catch(e){}</script>
13
17
  <link rel="manifest" href="/manifest.json">
14
18
  <link rel="apple-touch-icon" href="/icons/icon-192.png">
15
19
  <link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512.png">
@@ -17,47 +21,49 @@
17
21
  <link rel="stylesheet" href="/cookie-consent.css?v=1">
18
22
  </head>
19
23
  <body>
20
- <a href="#main-content" class="skip-link">Skip to main content</a>
24
+ <a href="#main-content" class="skip-link" data-i18n="nav.skipLink">Skip to main content</a>
21
25
  <div id="app">
22
26
  <!-- Auth Screen -->
23
27
  <div id="auth-screen" style="display:none">
24
28
  <div class="auth-left">
25
29
  <div class="auth-brand">
26
30
  <div class="auth-logo">pm<span>-web</span></div>
27
- <div class="auth-tagline">Git-native project management</div>
31
+ <div class="auth-tagline" data-i18n="auth.brand.tagline">Git-native project management</div>
28
32
  <ul class="auth-features">
29
- <li>Manage tasks, features, bugs, epics &amp; more</li>
30
- <li>Git-native storage in .agents/pm/</li>
31
- <li>Full history, comments &amp; activity feeds</li>
32
- <li>Team-friendly project isolation</li>
33
+ <li data-i18n="auth.feature.tasks">Manage tasks, features, bugs, epics &amp; more</li>
34
+ <li data-i18n="auth.feature.storage">Git-native storage in .agents/pm/</li>
35
+ <li data-i18n="auth.feature.history">Full history, comments &amp; activity feeds</li>
36
+ <li data-i18n="auth.feature.team">Team-friendly project isolation</li>
33
37
  </ul>
34
38
  </div>
35
39
  </div>
36
40
  <div class="auth-right">
37
- <div class="auth-form-title" id="auth-title">Welcome back</div>
38
- <div class="auth-form-sub" id="auth-sub">Sign in to your account to continue</div>
41
+ <div class="auth-form-title" id="auth-title" data-i18n="auth.title.login">Welcome back</div>
42
+ <div class="auth-form-sub" id="auth-sub" data-i18n="auth.sub.login">Sign in to your account to continue</div>
39
43
  <div class="auth-toggle">
40
- <button class="active" id="tab-login" onclick="window.__app.switchAuthTab('login')">Sign In</button>
41
- <button id="tab-register" onclick="window.__app.switchAuthTab('register')">Register</button>
44
+ <button class="active" id="tab-login" onclick="window.__app.switchAuthTab('login')" data-i18n="auth.tab.login">Sign In</button>
45
+ <button id="tab-register" onclick="window.__app.switchAuthTab('register')" data-i18n="auth.tab.register">Register</button>
42
46
  </div>
43
47
  <form id="auth-form" onsubmit="window.__app.submitAuth(event)">
44
48
  <div class="form-group" id="field-name" style="display:none">
45
- <label class="form-label">Display Name</label>
46
- <input class="form-input" type="text" id="auth-name" placeholder="Your name" autocomplete="name">
49
+ <label class="form-label" data-i18n="auth.field.displayName">Display Name</label>
50
+ <input class="form-input" type="text" id="auth-name" placeholder="Your name" data-i18n-placeholder="auth.placeholder.name" autocomplete="name">
47
51
  </div>
48
52
  <div class="form-group">
49
- <label class="form-label">Email</label>
53
+ <label class="form-label" data-i18n="auth.field.email">Email</label>
50
54
  <input class="form-input" type="email" id="auth-email" placeholder="you@example.com" required autocomplete="email">
51
55
  </div>
52
56
  <div class="form-group">
53
- <label class="form-label">Password</label>
57
+ <label class="form-label" data-i18n="auth.field.password">Password</label>
54
58
  <input class="form-input" type="password" id="auth-password" placeholder="••••••••" required autocomplete="current-password">
55
59
  </div>
56
60
  <div class="form-error" id="auth-error" style="display:none"></div>
57
61
  <button type="submit" class="btn btn-primary btn-full btn-lg" style="margin-top:8px" id="auth-submit">
58
- <span id="auth-btn-text">Sign In</span>
62
+ <span id="auth-btn-text" data-i18n="auth.button.login">Sign In</span>
59
63
  </button>
60
64
  </form>
65
+ <div id="oidc-divider" class="auth-form-sub" style="margin:18px 0 10px;text-align:center" hidden data-i18n="auth.divider">or</div>
66
+ <button id="oidc-login" type="button" class="btn btn-secondary btn-full btn-lg" hidden onclick="window.__app.startOidcLogin()" data-i18n="auth.oidc.default">Continue with OpenID Connect</button>
61
67
  </div>
62
68
  </div>
63
69
 
@@ -69,21 +75,21 @@
69
75
  <div class="project-selector-wrap">
70
76
  <label for="project-selector" class="sr-only">Select project</label>
71
77
  <select class="project-selector" id="project-selector" onchange="window.__app.onProjectSelect(this.value)" aria-label="Select project">
72
- <option value="">— Select project —</option>
78
+ <option value="" data-i18n="nav.selectProject">— Select project —</option>
73
79
  </select>
74
80
  </div>
75
81
  <div class="nav-spacer"></div>
76
- <div class="nav-search-hint" onclick="window.__app.openSearchModal()" title="Ctrl+K" role="button" tabindex="0" aria-label="Open search (Ctrl+K)" onkeydown="if(event.key==='Enter')window.__app.openSearchModal()">⌘K Search</div>
82
+ <div class="nav-search-hint" onclick="window.__app.openSearchModal()" title="Ctrl+K" role="button" tabindex="0" aria-label="Open search (Ctrl+K)" data-i18n-aria="nav.search.title" onkeydown="if(event.key==='Enter')window.__app.openSearchModal()" data-i18n="nav.search">⌘K Search</div>
77
83
  <div class="presence-bar" id="presence-bar" style="display:none" aria-label="Users viewing this project"></div>
78
- <div class="sse-indicator" id="sse-indicator" title="Real-time sync disconnected">
84
+ <div class="sse-indicator" id="sse-indicator" title="Real-time sync disconnected" data-i18n-title="nav.sse.disconnected">
79
85
  <div class="sse-dot"></div>
80
86
  </div>
81
- <button class="theme-toggle" id="theme-toggle" onclick="window.__app.cycleTheme()" title="Toggle theme (dark / light / auto)" aria-label="Toggle theme" tabindex="0">◐</button>
87
+ <button class="theme-toggle" id="theme-toggle" onclick="window.__app.cycleTheme()" title="Toggle theme (dark / light / auto)" data-i18n-title="nav.theme.title" aria-label="Toggle theme" data-i18n-aria="nav.theme.title" tabindex="0">◐</button>
82
88
  <div class="user-menu">
83
89
  <div class="user-avatar" id="user-avatar" aria-hidden="true">?</div>
84
90
  <span class="user-name" id="user-name-display"></span>
85
- <button class="btn btn-ghost btn-sm" onclick="window.__app.showView('settings')" style="margin-right:4px" aria-label="Open settings">Settings</button>
86
- <button class="btn btn-ghost btn-sm" onclick="window.__app.logout()" aria-label="Sign out">Sign out</button>
91
+ <button class="btn btn-ghost btn-sm" onclick="window.__app.showView('settings')" style="margin-right:4px" aria-label="Open settings" data-i18n-aria="nav.settings" data-i18n="nav.settings">Settings</button>
92
+ <button class="btn btn-ghost btn-sm" onclick="window.__app.logout()" aria-label="Sign out" data-i18n-aria="nav.signOut" data-i18n="nav.signOut">Sign out</button>
87
93
  </div>
88
94
  </nav>
89
95
  <div id="app-body">
@@ -217,15 +223,15 @@
217
223
 
218
224
  <!-- Offline Banner -->
219
225
  <div class="offline-banner" id="offline-banner">
220
- <span>You are offline — changes will sync when reconnected</span>
226
+ <span data-i18n="banner.offline">You are offline — changes will sync when reconnected</span>
221
227
  </div>
222
228
 
223
229
  <!-- PWA Install Banner -->
224
230
  <div class="install-banner" id="install-banner">
225
- <div class="install-banner-text">Install <strong>pm-web</strong> for a native app experience</div>
231
+ <div class="install-banner-text" data-i18n-html="banner.install.text">Install <strong>pm-web</strong> for a native app experience</div>
226
232
  <div style="display:flex;gap:8px">
227
- <button class="btn btn-ghost btn-sm" onclick="dismissInstallBanner()">Not now</button>
228
- <button class="btn btn-primary btn-sm" id="install-btn" onclick="installPwa()"><span>Install</span></button>
233
+ <button class="btn btn-ghost btn-sm" onclick="dismissInstallBanner()" data-i18n="banner.install.dismiss">Not now</button>
234
+ <button class="btn btn-primary btn-sm" id="install-btn" onclick="installPwa()"><span data-i18n="banner.install.button">Install</span></button>
229
235
  </div>
230
236
  </div>
231
237
 
@@ -260,7 +266,7 @@
260
266
  </nav>
261
267
 
262
268
  <script type="module" src="/src/app.js?v=9"></script>
263
- <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>
269
+ <div class="cookie-consent" id="cookie-consent" hidden><div class="cookie-consent-text"><strong data-i18n="cookie.banner.title">Cookie Settings</strong><span data-i18n="cookie.banner.text">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" data-i18n="cookie.banner.privacy">Privacy Policy</a><button type="button" class="btn btn-secondary btn-sm" data-cookie-decline data-i18n="cookie.banner.decline">Necessary only</button><button type="button" class="btn btn-primary btn-sm" data-cookie-accept data-i18n="cookie.banner.accept">Got it</button></div></div>
264
270
  <script src="/cookie-consent.js?v=1"></script>
265
271
  </body>
266
272
  </html>
@@ -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>