@unbrained/pm-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
package/public/sw.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SERVICE WORKER — pm-web PWA
|
|
3
|
+
// Cache versioning: auto-bust based on build timestamp
|
|
4
|
+
// Offline fallback page, mutation queue via IndexedDB
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
const BUILD_TIMESTAMP = '__BUILD_TIME__';
|
|
8
|
+
const CACHE_NAME = 'pm-web-' + (BUILD_TIMESTAMP !== '__BUILD_TIME__' ? BUILD_TIMESTAMP : Date.now().toString(36));
|
|
9
|
+
const MUTATION_DB = 'pm-web-offline';
|
|
10
|
+
const MUTATION_STORE = 'mutations';
|
|
11
|
+
|
|
12
|
+
const STATIC_ASSETS = [
|
|
13
|
+
'/',
|
|
14
|
+
'/styles.css',
|
|
15
|
+
'/manifest.json',
|
|
16
|
+
'/icons/icon-192.png',
|
|
17
|
+
'/icons/icon-512.png',
|
|
18
|
+
'/src/api.js',
|
|
19
|
+
'/src/app.js',
|
|
20
|
+
'/src/components/modals.js',
|
|
21
|
+
'/src/components/toast.js',
|
|
22
|
+
'/src/constants.js',
|
|
23
|
+
'/src/state.js',
|
|
24
|
+
'/src/types.js',
|
|
25
|
+
'/src/utils.js',
|
|
26
|
+
'/src/views/activity.js',
|
|
27
|
+
'/src/views/admin.js',
|
|
28
|
+
'/src/views/auth.js',
|
|
29
|
+
'/src/views/calendar.js',
|
|
30
|
+
'/src/views/comments-audit.js',
|
|
31
|
+
'/src/views/config.js',
|
|
32
|
+
'/src/views/context.js',
|
|
33
|
+
'/src/views/create.js',
|
|
34
|
+
'/src/views/dedupe.js',
|
|
35
|
+
'/src/views/export.js',
|
|
36
|
+
'/src/views/github.js',
|
|
37
|
+
'/src/views/graph-canvas.js',
|
|
38
|
+
'/src/views/graph.js',
|
|
39
|
+
'/src/views/groups.js',
|
|
40
|
+
'/src/views/guide.js',
|
|
41
|
+
'/src/views/health.js',
|
|
42
|
+
'/src/views/items.js',
|
|
43
|
+
'/src/views/normalize.js',
|
|
44
|
+
'/src/views/projects.js',
|
|
45
|
+
'/src/views/router.js',
|
|
46
|
+
'/src/views/search.js',
|
|
47
|
+
'/src/views/settings.js',
|
|
48
|
+
'/src/views/shared.js',
|
|
49
|
+
'/src/views/sharing.js',
|
|
50
|
+
'/src/views/stats.js',
|
|
51
|
+
'/src/views/templates.js',
|
|
52
|
+
'/src/views/validate.js',
|
|
53
|
+
'/src/views/plan.js',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── Offline fallback page ──
|
|
57
|
+
const OFFLINE_HTML = `<!DOCTYPE html>
|
|
58
|
+
<html lang="en">
|
|
59
|
+
<head>
|
|
60
|
+
<meta charset="UTF-8">
|
|
61
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
62
|
+
<title>pm-web — Offline</title>
|
|
63
|
+
<style>
|
|
64
|
+
body{font-family:'Inter',system-ui,sans-serif;background:#0a0f1e;color:#f1f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px;text-align:center}
|
|
65
|
+
.offline-icon{font-size:48px;margin-bottom:20px;opacity:0.5}
|
|
66
|
+
.offline-title{font-size:22px;font-weight:600;margin-bottom:8px}
|
|
67
|
+
.offline-text{color:#94a3b8;max-width:400px;line-height:1.7;margin-bottom:24px}
|
|
68
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:0.15s}
|
|
69
|
+
.btn-primary{background:#2dd4bf;color:#0f172a}
|
|
70
|
+
.btn-primary:hover{background:#34ead4}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div>
|
|
75
|
+
<div class="offline-icon">📡</div>
|
|
76
|
+
<div class="offline-title">You're offline</div>
|
|
77
|
+
<div class="offline-text">pm-web needs an internet connection to load. Please check your connection and try again.</div>
|
|
78
|
+
<button class="btn btn-primary" onclick="location.reload()">Try Again</button>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`;
|
|
82
|
+
|
|
83
|
+
// ── IndexedDB Mutation Queue ──
|
|
84
|
+
function openMutationDB(): Promise<IDBDatabase> {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const request = indexedDB.open(MUTATION_DB, 1);
|
|
87
|
+
request.onupgradeneeded = () => {
|
|
88
|
+
const db = request.result;
|
|
89
|
+
if (!db.objectStoreNames.contains(MUTATION_STORE)) {
|
|
90
|
+
const store = db.createObjectStore(MUTATION_STORE, { keyPath: 'id', autoIncrement: true });
|
|
91
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
request.onsuccess = () => resolve(request.result);
|
|
95
|
+
request.onerror = () => reject(request.error);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function queueMutation(method: string, path: string, body: unknown): Promise<void> {
|
|
100
|
+
try {
|
|
101
|
+
const db = await openMutationDB();
|
|
102
|
+
const tx = db.transaction(MUTATION_STORE, 'readwrite');
|
|
103
|
+
const store = tx.objectStore(MUTATION_STORE);
|
|
104
|
+
store.add({
|
|
105
|
+
method,
|
|
106
|
+
path,
|
|
107
|
+
body: body !== undefined ? JSON.stringify(body) : null,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// If IndexedDB fails, mutations are lost — graceful degradation
|
|
112
|
+
console.warn('Failed to queue mutation for offline:', e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getQueuedMutations(): Promise<Array<{ id: number; method: string; path: string; body: string | null; timestamp: number }>> {
|
|
117
|
+
try {
|
|
118
|
+
const db = await openMutationDB();
|
|
119
|
+
const tx = db.transaction(MUTATION_STORE, 'readonly');
|
|
120
|
+
const store = tx.objectStore(MUTATION_STORE);
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const request = store.getAll();
|
|
123
|
+
request.onsuccess = () => resolve(request.result);
|
|
124
|
+
request.onerror = () => reject(request.error);
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function clearMutation(id: number): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const db = await openMutationDB();
|
|
134
|
+
const tx = db.transaction(MUTATION_STORE, 'readwrite');
|
|
135
|
+
tx.objectStore(MUTATION_STORE).delete(id);
|
|
136
|
+
} catch { /* ignore */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function flushMutationQueue(): Promise<void> {
|
|
140
|
+
const mutations = await getQueuedMutations();
|
|
141
|
+
if (mutations.length === 0) return;
|
|
142
|
+
|
|
143
|
+
for (const mut of mutations) {
|
|
144
|
+
try {
|
|
145
|
+
const opts: RequestInit = {
|
|
146
|
+
method: mut.method,
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
credentials: 'include',
|
|
149
|
+
};
|
|
150
|
+
if (mut.body !== null) opts.body = mut.body;
|
|
151
|
+
const res = await fetch('/api' + mut.path, opts);
|
|
152
|
+
if (res.ok) {
|
|
153
|
+
await clearMutation(mut.id);
|
|
154
|
+
} else {
|
|
155
|
+
console.warn('Offline mutation failed:', mut.method, mut.path, res.status);
|
|
156
|
+
// Stop processing on first failure — try again later
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Network failed again — stop processing
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Notify clients about replayed mutations
|
|
166
|
+
const remaining = await getQueuedMutations();
|
|
167
|
+
if (remaining.length === 0 && mutations.length > 0) {
|
|
168
|
+
const clients = await self.clients.matchAll();
|
|
169
|
+
clients.forEach(client => {
|
|
170
|
+
client.postMessage({ type: 'MUTATIONS_REPLAYED', count: mutations.length });
|
|
171
|
+
});
|
|
172
|
+
} else if (remaining.length > 0) {
|
|
173
|
+
const clients = await self.clients.matchAll();
|
|
174
|
+
clients.forEach(client => {
|
|
175
|
+
client.postMessage({ type: 'MUTATIONS_PARTIAL', replayed: mutations.length - remaining.length, remaining: remaining.length });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Install ──
|
|
181
|
+
self.addEventListener('install', (event) => {
|
|
182
|
+
event.waitUntil(
|
|
183
|
+
caches.open(CACHE_NAME).then((cache) =>
|
|
184
|
+
Promise.all(STATIC_ASSETS.map((asset) => cache.add(asset).catch(() => null)))
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
self.skipWaiting();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Activate ──
|
|
191
|
+
self.addEventListener('activate', (event) => {
|
|
192
|
+
event.waitUntil(
|
|
193
|
+
caches.keys().then((keys) =>
|
|
194
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
self.clients.claim();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── Fetch strategy ──
|
|
201
|
+
self.addEventListener('fetch', (event) => {
|
|
202
|
+
const url = new URL(event.request.url);
|
|
203
|
+
|
|
204
|
+
// API calls: try network, queue mutations if offline
|
|
205
|
+
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/healthz')) {
|
|
206
|
+
// Queue write operations (POST, PUT, PATCH, DELETE) when offline
|
|
207
|
+
if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
|
|
208
|
+
event.respondWith(
|
|
209
|
+
fetch(event.request).catch(async () => {
|
|
210
|
+
// Network failed — queue the mutation for later
|
|
211
|
+
let body: unknown = undefined;
|
|
212
|
+
try {
|
|
213
|
+
body = await event.request.clone().json();
|
|
214
|
+
} catch { /* no body */ }
|
|
215
|
+
await queueMutation(event.request.method, url.pathname.replace('/api', ''), body);
|
|
216
|
+
return new Response(JSON.stringify({ queued: true, message: 'Request queued for when you are back online' }), {
|
|
217
|
+
status: 202,
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
});
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// GET/HEAD API calls: network-only, return offline JSON error
|
|
226
|
+
event.respondWith(
|
|
227
|
+
fetch(event.request)
|
|
228
|
+
.catch(() => new Response(JSON.stringify({ error: 'Offline — check your connection', queued: 0 }), {
|
|
229
|
+
status: 503,
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
}))
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Navigation (SPA shell): network-first, cache fallback, offline page fallback
|
|
237
|
+
if (event.request.mode === 'navigate') {
|
|
238
|
+
event.respondWith(
|
|
239
|
+
fetch(event.request)
|
|
240
|
+
.then((res) => {
|
|
241
|
+
if (res.ok) {
|
|
242
|
+
const clone = res.clone();
|
|
243
|
+
caches.open(CACHE_NAME).then((c) => c.put('/', clone));
|
|
244
|
+
}
|
|
245
|
+
return res;
|
|
246
|
+
})
|
|
247
|
+
.catch(async () => {
|
|
248
|
+
// Try cached shell first
|
|
249
|
+
const cached = await caches.match('/');
|
|
250
|
+
if (cached) return cached;
|
|
251
|
+
// Return offline fallback page
|
|
252
|
+
return new Response(OFFLINE_HTML, {
|
|
253
|
+
status: 503,
|
|
254
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
255
|
+
});
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Static assets: stale-while-revalidate
|
|
262
|
+
if (
|
|
263
|
+
url.pathname.endsWith('.css') ||
|
|
264
|
+
url.pathname.endsWith('.js') ||
|
|
265
|
+
url.pathname.endsWith('.json') ||
|
|
266
|
+
url.pathname.endsWith('.png') ||
|
|
267
|
+
url.pathname.endsWith('.svg') ||
|
|
268
|
+
url.pathname.endsWith('.ico') ||
|
|
269
|
+
url.pathname.endsWith('.woff2') ||
|
|
270
|
+
url.hostname.includes('fonts.googleapis.com') ||
|
|
271
|
+
url.hostname.includes('fonts.gstatic.com')
|
|
272
|
+
) {
|
|
273
|
+
event.respondWith(
|
|
274
|
+
caches.open(CACHE_NAME).then((cache) =>
|
|
275
|
+
cache.match(event.request).then((cached) => {
|
|
276
|
+
const fetched = fetch(event.request).then((res) => {
|
|
277
|
+
if (res.ok) cache.put(event.request, res.clone());
|
|
278
|
+
return res;
|
|
279
|
+
}).catch(() => cached);
|
|
280
|
+
return cached || fetched;
|
|
281
|
+
})
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Default: network, fallback to cache
|
|
288
|
+
event.respondWith(
|
|
289
|
+
fetch(event.request)
|
|
290
|
+
.catch(() => caches.match(event.request))
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── Messages ──
|
|
295
|
+
self.addEventListener('message', (event) => {
|
|
296
|
+
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
297
|
+
self.skipWaiting();
|
|
298
|
+
}
|
|
299
|
+
if (event.data && event.data.type === 'CACHE_URLS') {
|
|
300
|
+
const urls = event.data.urls || [];
|
|
301
|
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(urls).catch(() => {}));
|
|
302
|
+
}
|
|
303
|
+
if (event.data && event.data.type === 'FLUSH_QUEUE') {
|
|
304
|
+
flushMutationQueue();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── Background sync ──
|
|
309
|
+
self.addEventListener('sync', (event) => {
|
|
310
|
+
if (event.tag === 'pm-sync') {
|
|
311
|
+
event.waitUntil(flushMutationQueue());
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ── Online event: flush queue when connectivity returns ──
|
|
316
|
+
self.addEventListener('online', () => {
|
|
317
|
+
flushMutationQueue();
|
|
318
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": ".",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
16
|
+
"noEmit": false
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
package/sql/schema.sql
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
-- pm-web schema
|
|
2
|
+
CREATE TABLE IF NOT EXISTS pm_users (
|
|
3
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
email TEXT UNIQUE NOT NULL,
|
|
5
|
+
password_hash TEXT NOT NULL,
|
|
6
|
+
display_name TEXT,
|
|
7
|
+
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
8
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
9
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE TABLE IF NOT EXISTS pm_projects (
|
|
13
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
14
|
+
user_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
slug TEXT NOT NULL,
|
|
17
|
+
description TEXT DEFAULT '',
|
|
18
|
+
prefix TEXT NOT NULL,
|
|
19
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
20
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
21
|
+
UNIQUE(user_id, slug)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS pm_projects_user_id ON pm_projects(user_id);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS pm_groups (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
owner_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
29
|
+
name TEXT NOT NULL,
|
|
30
|
+
description TEXT DEFAULT '',
|
|
31
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
32
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS pm_group_members (
|
|
36
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
+
group_id UUID NOT NULL REFERENCES pm_groups(id) ON DELETE CASCADE,
|
|
38
|
+
user_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
39
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
40
|
+
invited_at TIMESTAMPTZ DEFAULT NOW(),
|
|
41
|
+
UNIQUE(group_id, user_id)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS pm_project_shares (
|
|
45
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
46
|
+
project_id UUID NOT NULL REFERENCES pm_projects(id) ON DELETE CASCADE,
|
|
47
|
+
shared_with_user_id UUID REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
48
|
+
shared_with_group_id UUID REFERENCES pm_groups(id) ON DELETE CASCADE,
|
|
49
|
+
permission TEXT NOT NULL DEFAULT 'view',
|
|
50
|
+
shared_at TIMESTAMPTZ DEFAULT NOW(),
|
|
51
|
+
CONSTRAINT share_target CHECK (
|
|
52
|
+
(shared_with_user_id IS NOT NULL AND shared_with_group_id IS NULL) OR
|
|
53
|
+
(shared_with_user_id IS NULL AND shared_with_group_id IS NOT NULL)
|
|
54
|
+
),
|
|
55
|
+
UNIQUE(project_id, shared_with_user_id),
|
|
56
|
+
UNIQUE(project_id, shared_with_group_id)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
-- Encrypted GitHub PAT payload: pmweb:v1:<iv>:<tag>:<ciphertext>
|
|
60
|
+
ALTER TABLE pm_users ADD COLUMN IF NOT EXISTS github_token TEXT;
|
|
61
|
+
ALTER TABLE pm_users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE;
|
|
62
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_owner TEXT;
|
|
63
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_repo TEXT;
|
|
64
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_sync_enabled BOOLEAN DEFAULT FALSE;
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS pm_admin_audit (
|
|
67
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
68
|
+
actor_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
69
|
+
action TEXT NOT NULL,
|
|
70
|
+
description TEXT DEFAULT '',
|
|
71
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS pm_admin_audit_created_at ON pm_admin_audit(created_at DESC);
|
|
75
|
+
|
|
76
|
+
-- GitHub item links: tracks pm items pushed to GitHub issues for two-way sync
|
|
77
|
+
CREATE TABLE IF NOT EXISTS pm_github_item_links (
|
|
78
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
79
|
+
project_id UUID NOT NULL REFERENCES pm_projects(id) ON DELETE CASCADE,
|
|
80
|
+
pm_item_id TEXT NOT NULL,
|
|
81
|
+
issue_number INTEGER NOT NULL,
|
|
82
|
+
issue_url TEXT NOT NULL,
|
|
83
|
+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
84
|
+
UNIQUE (project_id, pm_item_id)
|
|
85
|
+
);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS pm_github_item_links_project ON pm_github_item_links(project_id);
|
|
87
|
+
|
|
88
|
+
-- Bootstrap admin promotion is now applied at runtime via PM_WEB_BOOTSTRAP_ADMIN_EMAIL (see src/db.ts).
|
|
89
|
+
|
|
90
|
+
-- Update trigger
|
|
91
|
+
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
92
|
+
RETURNS TRIGGER AS $$
|
|
93
|
+
BEGIN
|
|
94
|
+
NEW.updated_at = NOW();
|
|
95
|
+
RETURN NEW;
|
|
96
|
+
END;
|
|
97
|
+
$$ LANGUAGE plpgsql;
|
|
98
|
+
|
|
99
|
+
CREATE OR REPLACE TRIGGER pm_users_updated_at
|
|
100
|
+
BEFORE UPDATE ON pm_users
|
|
101
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
102
|
+
|
|
103
|
+
CREATE OR REPLACE TRIGGER pm_projects_updated_at
|
|
104
|
+
BEFORE UPDATE ON pm_projects
|
|
105
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|