@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.
- package/CHANGELOG.md +19 -0
- package/README.md +26 -0
- package/dist/app.d.ts +16 -1
- package/dist/app.js +53 -3
- 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 +172 -108
- 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 +27 -3
- package/dist/services/pm-runner.js +188 -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 +32 -26
- package/public/legal-notice.html +10 -12
- package/public/privacy-policy.html +14 -24
- package/public/src/app.js +18 -4
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +19 -4
- package/public/src/components/modals.js +5 -3
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +6 -3
- package/public/src/components/toast.js.map +1 -1
- package/public/src/cookie-consent.ts +68 -0
- package/public/src/i18n/de.json +117 -0
- package/public/src/i18n/en.json +117 -0
- package/public/src/i18n/es.json +117 -0
- package/public/src/i18n/zh.json +117 -0
- package/public/src/i18n.js +287 -0
- package/public/src/i18n.js.map +1 -0
- package/public/src/i18n.ts +306 -0
- package/public/src/sw.ts +446 -0
- package/public/src/views/auth.js +29 -6
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +27 -6
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/settings.js +73 -48
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +77 -48
- package/public/styles.css +2 -0
- package/public/sw.js +323 -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.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.
|
|
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 &&
|
|
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.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.
|
|
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": "^
|
|
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
|
@@ -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 & more</li>
|
|
30
|
-
<li>Git-native storage in .agents/pm/</li>
|
|
31
|
-
<li>Full history, comments & activity feeds</li>
|
|
32
|
-
<li>Team-friendly project isolation</li>
|
|
33
|
+
<li data-i18n="auth.feature.tasks">Manage tasks, features, bugs, epics & 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 & 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>
|
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>
|