@siteboon/claude-code-ui 1.25.2 → 1.26.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/README.de.md +239 -0
- package/README.ja.md +115 -230
- package/README.ko.md +116 -231
- package/README.md +2 -1
- package/README.ru.md +75 -54
- package/README.zh-CN.md +121 -238
- package/dist/assets/index-C08k8QbP.css +32 -0
- package/dist/assets/{index-DF_FFT3b.js → index-DnXcHp5q.js} +249 -242
- package/dist/index.html +2 -2
- package/dist/sw.js +59 -3
- package/package.json +3 -2
- package/server/claude-sdk.js +106 -62
- package/server/cli.js +10 -7
- package/server/cursor-cli.js +59 -73
- package/server/database/db.js +142 -1
- package/server/database/init.sql +28 -1
- package/server/gemini-cli.js +46 -48
- package/server/gemini-response-handler.js +12 -73
- package/server/index.js +82 -55
- package/server/middleware/auth.js +2 -2
- package/server/openai-codex.js +43 -28
- package/server/projects.js +1 -1
- package/server/providers/claude/adapter.js +278 -0
- package/server/providers/codex/adapter.js +248 -0
- package/server/providers/cursor/adapter.js +353 -0
- package/server/providers/gemini/adapter.js +186 -0
- package/server/providers/registry.js +44 -0
- package/server/providers/types.js +119 -0
- package/server/providers/utils.js +29 -0
- package/server/routes/agent.js +7 -5
- package/server/routes/cli-auth.js +38 -0
- package/server/routes/codex.js +1 -19
- package/server/routes/gemini.js +0 -30
- package/server/routes/git.js +48 -20
- package/server/routes/messages.js +61 -0
- package/server/routes/plugins.js +5 -1
- package/server/routes/settings.js +99 -1
- package/server/routes/taskmaster.js +2 -2
- package/server/services/notification-orchestrator.js +227 -0
- package/server/services/vapid-keys.js +35 -0
- package/server/utils/plugin-loader.js +53 -4
- package/shared/networkHosts.js +22 -0
- package/dist/assets/index-WNTmA_ug.css +0 -32
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module providers/utils
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prefixes that indicate internal/system content which should be hidden from the UI.
|
|
9
|
+
* @type {readonly string[]}
|
|
10
|
+
*/
|
|
11
|
+
export const INTERNAL_CONTENT_PREFIXES = Object.freeze([
|
|
12
|
+
'<command-name>',
|
|
13
|
+
'<command-message>',
|
|
14
|
+
'<command-args>',
|
|
15
|
+
'<local-command-stdout>',
|
|
16
|
+
'<system-reminder>',
|
|
17
|
+
'Caveat:',
|
|
18
|
+
'This session is being continued from a previous',
|
|
19
|
+
'[Request interrupted',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if user text content is internal/system that should be skipped.
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function isInternalContent(content) {
|
|
28
|
+
return INTERNAL_CONTENT_PREFIXES.some(prefix => content.startsWith(prefix));
|
|
29
|
+
}
|
package/server/routes/agent.js
CHANGED
|
@@ -450,9 +450,10 @@ async function cleanupProject(projectPath, sessionId = null) {
|
|
|
450
450
|
* SSE Stream Writer - Adapts SDK/CLI output to Server-Sent Events
|
|
451
451
|
*/
|
|
452
452
|
class SSEStreamWriter {
|
|
453
|
-
constructor(res) {
|
|
453
|
+
constructor(res, userId = null) {
|
|
454
454
|
this.res = res;
|
|
455
455
|
this.sessionId = null;
|
|
456
|
+
this.userId = userId;
|
|
456
457
|
this.isSSEStreamWriter = true; // Marker for transport detection
|
|
457
458
|
}
|
|
458
459
|
|
|
@@ -485,9 +486,10 @@ class SSEStreamWriter {
|
|
|
485
486
|
* Non-streaming response collector
|
|
486
487
|
*/
|
|
487
488
|
class ResponseCollector {
|
|
488
|
-
constructor() {
|
|
489
|
+
constructor(userId = null) {
|
|
489
490
|
this.messages = [];
|
|
490
491
|
this.sessionId = null;
|
|
492
|
+
this.userId = userId;
|
|
491
493
|
}
|
|
492
494
|
|
|
493
495
|
send(data) {
|
|
@@ -920,7 +922,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
920
922
|
res.setHeader('Connection', 'keep-alive');
|
|
921
923
|
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
922
924
|
|
|
923
|
-
writer = new SSEStreamWriter(res);
|
|
925
|
+
writer = new SSEStreamWriter(res, req.user.id);
|
|
924
926
|
|
|
925
927
|
// Send initial status
|
|
926
928
|
writer.send({
|
|
@@ -930,7 +932,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
930
932
|
});
|
|
931
933
|
} else {
|
|
932
934
|
// Non-streaming mode: collect messages
|
|
933
|
-
writer = new ResponseCollector();
|
|
935
|
+
writer = new ResponseCollector(req.user.id);
|
|
934
936
|
|
|
935
937
|
// Collect initial status message
|
|
936
938
|
writer.send({
|
|
@@ -1219,7 +1221,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1219
1221
|
res.setHeader('Cache-Control', 'no-cache');
|
|
1220
1222
|
res.setHeader('Connection', 'keep-alive');
|
|
1221
1223
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
1222
|
-
writer = new SSEStreamWriter(res);
|
|
1224
|
+
writer = new SSEStreamWriter(res, req.user.id);
|
|
1223
1225
|
}
|
|
1224
1226
|
|
|
1225
1227
|
if (!res.writableEnded) {
|
|
@@ -96,10 +96,27 @@ router.get('/gemini/status', async (req, res) => {
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
async function loadClaudeSettingsEnv() {
|
|
100
|
+
try {
|
|
101
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
102
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
103
|
+
const settings = JSON.parse(content);
|
|
104
|
+
|
|
105
|
+
if (settings?.env && typeof settings.env === 'object') {
|
|
106
|
+
return settings.env;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Ignore missing or malformed settings and fall back to other auth sources.
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
/**
|
|
100
116
|
* Checks Claude authentication credentials using two methods with priority order:
|
|
101
117
|
*
|
|
102
118
|
* Priority 1: ANTHROPIC_API_KEY environment variable
|
|
119
|
+
* Priority 1b: ~/.claude/settings.json env values
|
|
103
120
|
* Priority 2: ~/.claude/.credentials.json OAuth tokens
|
|
104
121
|
*
|
|
105
122
|
* The Claude Agent SDK prioritizes environment variables over authenticated subscriptions.
|
|
@@ -128,6 +145,27 @@ async function checkClaudeCredentials() {
|
|
|
128
145
|
};
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
// Priority 1b: Check ~/.claude/settings.json env values.
|
|
149
|
+
// Claude Code can read proxy/auth values from settings.json even when the
|
|
150
|
+
// CloudCLI server process itself was not started with those env vars exported.
|
|
151
|
+
const settingsEnv = await loadClaudeSettingsEnv();
|
|
152
|
+
|
|
153
|
+
if (typeof settingsEnv.ANTHROPIC_API_KEY === 'string' && settingsEnv.ANTHROPIC_API_KEY.trim()) {
|
|
154
|
+
return {
|
|
155
|
+
authenticated: true,
|
|
156
|
+
email: 'API Key Auth',
|
|
157
|
+
method: 'api_key'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof settingsEnv.ANTHROPIC_AUTH_TOKEN === 'string' && settingsEnv.ANTHROPIC_AUTH_TOKEN.trim()) {
|
|
162
|
+
return {
|
|
163
|
+
authenticated: true,
|
|
164
|
+
email: 'Configured via settings.json',
|
|
165
|
+
method: 'api_key'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
131
169
|
// Priority 2: Check ~/.claude/.credentials.json for OAuth tokens
|
|
132
170
|
// This is the standard authentication method used by Claude CLI after running
|
|
133
171
|
// 'claude /login' or 'claude setup-token' commands.
|
package/server/routes/codex.js
CHANGED
|
@@ -4,7 +4,7 @@ import { promises as fs } from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import TOML from '@iarna/toml';
|
|
7
|
-
import { getCodexSessions,
|
|
7
|
+
import { getCodexSessions, deleteCodexSession } from '../projects.js';
|
|
8
8
|
import { applyCustomSessionNames, sessionNamesDb } from '../database/db.js';
|
|
9
9
|
|
|
10
10
|
const router = express.Router();
|
|
@@ -68,24 +68,6 @@ router.get('/sessions', async (req, res) => {
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
router.get('/sessions/:sessionId/messages', async (req, res) => {
|
|
72
|
-
try {
|
|
73
|
-
const { sessionId } = req.params;
|
|
74
|
-
const { limit, offset } = req.query;
|
|
75
|
-
|
|
76
|
-
const result = await getCodexSessionMessages(
|
|
77
|
-
sessionId,
|
|
78
|
-
limit ? parseInt(limit, 10) : null,
|
|
79
|
-
offset ? parseInt(offset, 10) : 0
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
res.json({ success: true, ...result });
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error('Error fetching Codex session messages:', error);
|
|
85
|
-
res.status(500).json({ success: false, error: error.message });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
71
|
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
90
72
|
try {
|
|
91
73
|
const { sessionId } = req.params;
|
package/server/routes/gemini.js
CHANGED
|
@@ -1,39 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import sessionManager from '../sessionManager.js';
|
|
3
3
|
import { sessionNamesDb } from '../database/db.js';
|
|
4
|
-
import { getGeminiCliSessionMessages } from '../projects.js';
|
|
5
4
|
|
|
6
5
|
const router = express.Router();
|
|
7
6
|
|
|
8
|
-
router.get('/sessions/:sessionId/messages', async (req, res) => {
|
|
9
|
-
try {
|
|
10
|
-
const { sessionId } = req.params;
|
|
11
|
-
|
|
12
|
-
if (!sessionId || typeof sessionId !== 'string' || !/^[a-zA-Z0-9_.-]{1,100}$/.test(sessionId)) {
|
|
13
|
-
return res.status(400).json({ success: false, error: 'Invalid session ID format' });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let messages = sessionManager.getSessionMessages(sessionId);
|
|
17
|
-
|
|
18
|
-
// Fallback to Gemini CLI sessions on disk
|
|
19
|
-
if (messages.length === 0) {
|
|
20
|
-
messages = await getGeminiCliSessionMessages(sessionId);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
res.json({
|
|
24
|
-
success: true,
|
|
25
|
-
messages: messages,
|
|
26
|
-
total: messages.length,
|
|
27
|
-
hasMore: false,
|
|
28
|
-
offset: 0,
|
|
29
|
-
limit: messages.length
|
|
30
|
-
});
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.error('Error fetching Gemini session messages:', error);
|
|
33
|
-
res.status(500).json({ success: false, error: error.message });
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
7
|
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
38
8
|
try {
|
|
39
9
|
const { sessionId } = req.params;
|
package/server/routes/git.js
CHANGED
|
@@ -651,26 +651,28 @@ router.get('/branches', async (req, res) => {
|
|
|
651
651
|
|
|
652
652
|
// Get all branches
|
|
653
653
|
const { stdout } = await spawnAsync('git', ['branch', '-a'], { cwd: projectPath });
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const branches = stdout
|
|
654
|
+
|
|
655
|
+
const rawLines = stdout
|
|
657
656
|
.split('\n')
|
|
658
|
-
.map(
|
|
659
|
-
.filter(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
657
|
+
.map(b => b.trim())
|
|
658
|
+
.filter(b => b && !b.includes('->'));
|
|
659
|
+
|
|
660
|
+
// Local branches (may start with '* ' for current)
|
|
661
|
+
const localBranches = rawLines
|
|
662
|
+
.filter(b => !b.startsWith('remotes/'))
|
|
663
|
+
.map(b => (b.startsWith('* ') ? b.substring(2) : b));
|
|
664
|
+
|
|
665
|
+
// Remote branches — strip 'remotes/<remote>/' prefix
|
|
666
|
+
const remoteBranches = rawLines
|
|
667
|
+
.filter(b => b.startsWith('remotes/'))
|
|
668
|
+
.map(b => b.replace(/^remotes\/[^/]+\//, ''))
|
|
669
|
+
.filter(name => !localBranches.includes(name)); // skip if already a local branch
|
|
670
|
+
|
|
671
|
+
// Backward-compat flat list (local + unique remotes, deduplicated)
|
|
672
|
+
const branches = [...localBranches, ...remoteBranches]
|
|
673
|
+
.filter((b, i, arr) => arr.indexOf(b) === i);
|
|
674
|
+
|
|
675
|
+
res.json({ branches, localBranches, remoteBranches });
|
|
674
676
|
} catch (error) {
|
|
675
677
|
console.error('Git branches error:', error);
|
|
676
678
|
res.json({ error: error.message });
|
|
@@ -721,6 +723,32 @@ router.post('/create-branch', async (req, res) => {
|
|
|
721
723
|
}
|
|
722
724
|
});
|
|
723
725
|
|
|
726
|
+
// Delete a local branch
|
|
727
|
+
router.post('/delete-branch', async (req, res) => {
|
|
728
|
+
const { project, branch } = req.body;
|
|
729
|
+
|
|
730
|
+
if (!project || !branch) {
|
|
731
|
+
return res.status(400).json({ error: 'Project name and branch name are required' });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const projectPath = await getActualProjectPath(project);
|
|
736
|
+
await validateGitRepository(projectPath);
|
|
737
|
+
|
|
738
|
+
// Safety: cannot delete the currently checked-out branch
|
|
739
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['branch', '--show-current'], { cwd: projectPath });
|
|
740
|
+
if (currentBranch.trim() === branch) {
|
|
741
|
+
return res.status(400).json({ error: 'Cannot delete the currently checked-out branch' });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const { stdout } = await spawnAsync('git', ['branch', '-d', branch], { cwd: projectPath });
|
|
745
|
+
res.json({ success: true, output: stdout });
|
|
746
|
+
} catch (error) {
|
|
747
|
+
console.error('Git delete branch error:', error);
|
|
748
|
+
res.status(500).json({ error: error.message });
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
724
752
|
// Get recent commits
|
|
725
753
|
router.get('/commits', async (req, res) => {
|
|
726
754
|
const { project, limit = 10 } = req.query;
|
|
@@ -740,7 +768,7 @@ router.get('/commits', async (req, res) => {
|
|
|
740
768
|
// Get commit log with stats
|
|
741
769
|
const { stdout } = await spawnAsync(
|
|
742
770
|
'git',
|
|
743
|
-
['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=
|
|
771
|
+
['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=iso-strict', '-n', String(safeLimit)],
|
|
744
772
|
{ cwd: projectPath },
|
|
745
773
|
);
|
|
746
774
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified messages endpoint.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/sessions/:sessionId/messages?provider=claude&projectName=foo&limit=50&offset=0
|
|
5
|
+
*
|
|
6
|
+
* Replaces the four provider-specific session message endpoints with a single route
|
|
7
|
+
* that delegates to the appropriate adapter via the provider registry.
|
|
8
|
+
*
|
|
9
|
+
* @module routes/messages
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import { getProvider, getAllProviders } from '../providers/registry.js';
|
|
14
|
+
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GET /api/sessions/:sessionId/messages
|
|
19
|
+
*
|
|
20
|
+
* Auth: authenticateToken applied at mount level in index.js
|
|
21
|
+
*
|
|
22
|
+
* Query params:
|
|
23
|
+
* provider - 'claude' | 'cursor' | 'codex' | 'gemini' (default: 'claude')
|
|
24
|
+
* projectName - required for claude provider
|
|
25
|
+
* projectPath - required for cursor provider (absolute path used for cwdId hash)
|
|
26
|
+
* limit - page size (omit or null for all)
|
|
27
|
+
* offset - pagination offset (default: 0)
|
|
28
|
+
*/
|
|
29
|
+
router.get('/:sessionId/messages', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const { sessionId } = req.params;
|
|
32
|
+
const provider = req.query.provider || 'claude';
|
|
33
|
+
const projectName = req.query.projectName || '';
|
|
34
|
+
const projectPath = req.query.projectPath || '';
|
|
35
|
+
const limitParam = req.query.limit;
|
|
36
|
+
const limit = limitParam !== undefined && limitParam !== null && limitParam !== ''
|
|
37
|
+
? parseInt(limitParam, 10)
|
|
38
|
+
: null;
|
|
39
|
+
const offset = parseInt(req.query.offset || '0', 10);
|
|
40
|
+
|
|
41
|
+
const adapter = getProvider(provider);
|
|
42
|
+
if (!adapter) {
|
|
43
|
+
const available = getAllProviders().join(', ');
|
|
44
|
+
return res.status(400).json({ error: `Unknown provider: ${provider}. Available: ${available}` });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await adapter.fetchHistory(sessionId, {
|
|
48
|
+
projectName,
|
|
49
|
+
projectPath,
|
|
50
|
+
limit,
|
|
51
|
+
offset,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return res.json(result);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error fetching unified messages:', error);
|
|
57
|
+
return res.status(500).json({ error: 'Failed to fetch messages' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default router;
|
package/server/routes/plugins.js
CHANGED
|
@@ -81,6 +81,10 @@ router.get('/:name/assets/*', (req, res) => {
|
|
|
81
81
|
|
|
82
82
|
const contentType = mime.lookup(resolvedPath) || 'application/octet-stream';
|
|
83
83
|
res.setHeader('Content-Type', contentType);
|
|
84
|
+
// Prevent CDN/proxy caching of plugin assets so updates take effect immediately
|
|
85
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
86
|
+
res.setHeader('Pragma', 'no-cache');
|
|
87
|
+
res.setHeader('Expires', '0');
|
|
84
88
|
const stream = fs.createReadStream(resolvedPath);
|
|
85
89
|
stream.on('error', () => {
|
|
86
90
|
if (!res.headersSent) {
|
|
@@ -236,7 +240,7 @@ router.all('/:name/rpc/*', async (req, res) => {
|
|
|
236
240
|
'content-type': req.headers['content-type'] || 'application/json',
|
|
237
241
|
};
|
|
238
242
|
|
|
239
|
-
// Add per-plugin secrets as X-Plugin-Secret-* headers
|
|
243
|
+
// Add per-plugin user-configured secrets as X-Plugin-Secret-* headers
|
|
240
244
|
for (const [key, value] of Object.entries(secrets)) {
|
|
241
245
|
headers[`x-plugin-secret-${key.toLowerCase()}`] = String(value);
|
|
242
246
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { apiKeysDb, credentialsDb } from '../database/db.js';
|
|
2
|
+
import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../database/db.js';
|
|
3
|
+
import { getPublicKey } from '../services/vapid-keys.js';
|
|
4
|
+
import { createNotificationEvent, notifyUserIfEnabled } from '../services/notification-orchestrator.js';
|
|
3
5
|
|
|
4
6
|
const router = express.Router();
|
|
5
7
|
|
|
@@ -175,4 +177,100 @@ router.patch('/credentials/:credentialId/toggle', async (req, res) => {
|
|
|
175
177
|
}
|
|
176
178
|
});
|
|
177
179
|
|
|
180
|
+
// ===============================
|
|
181
|
+
// Notification Preferences
|
|
182
|
+
// ===============================
|
|
183
|
+
|
|
184
|
+
router.get('/notification-preferences', async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const preferences = notificationPreferencesDb.getPreferences(req.user.id);
|
|
187
|
+
res.json({ success: true, preferences });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error('Error fetching notification preferences:', error);
|
|
190
|
+
res.status(500).json({ error: 'Failed to fetch notification preferences' });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
router.put('/notification-preferences', async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const preferences = notificationPreferencesDb.updatePreferences(req.user.id, req.body || {});
|
|
197
|
+
res.json({ success: true, preferences });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Error saving notification preferences:', error);
|
|
200
|
+
res.status(500).json({ error: 'Failed to save notification preferences' });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ===============================
|
|
205
|
+
// Push Subscription Management
|
|
206
|
+
// ===============================
|
|
207
|
+
|
|
208
|
+
router.get('/push/vapid-public-key', async (req, res) => {
|
|
209
|
+
try {
|
|
210
|
+
const publicKey = getPublicKey();
|
|
211
|
+
res.json({ publicKey });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('Error fetching VAPID public key:', error);
|
|
214
|
+
res.status(500).json({ error: 'Failed to fetch VAPID public key' });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
router.post('/push/subscribe', async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const { endpoint, keys } = req.body;
|
|
221
|
+
if (!endpoint || !keys?.p256dh || !keys?.auth) {
|
|
222
|
+
return res.status(400).json({ error: 'Missing subscription fields' });
|
|
223
|
+
}
|
|
224
|
+
pushSubscriptionsDb.saveSubscription(req.user.id, endpoint, keys.p256dh, keys.auth);
|
|
225
|
+
|
|
226
|
+
// Enable webPush in preferences so the confirmation goes through the full pipeline
|
|
227
|
+
const currentPrefs = notificationPreferencesDb.getPreferences(req.user.id);
|
|
228
|
+
if (!currentPrefs?.channels?.webPush) {
|
|
229
|
+
notificationPreferencesDb.updatePreferences(req.user.id, {
|
|
230
|
+
...currentPrefs,
|
|
231
|
+
channels: { ...currentPrefs?.channels, webPush: true },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
res.json({ success: true });
|
|
236
|
+
|
|
237
|
+
// Send a confirmation push through the full notification pipeline
|
|
238
|
+
const event = createNotificationEvent({
|
|
239
|
+
provider: 'system',
|
|
240
|
+
kind: 'info',
|
|
241
|
+
code: 'push.enabled',
|
|
242
|
+
meta: { message: 'Push notifications are now enabled!' },
|
|
243
|
+
severity: 'info'
|
|
244
|
+
});
|
|
245
|
+
notifyUserIfEnabled({ userId: req.user.id, event });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('Error saving push subscription:', error);
|
|
248
|
+
res.status(500).json({ error: 'Failed to save push subscription' });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
router.post('/push/unsubscribe', async (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const { endpoint } = req.body;
|
|
255
|
+
if (!endpoint) {
|
|
256
|
+
return res.status(400).json({ error: 'Missing endpoint' });
|
|
257
|
+
}
|
|
258
|
+
pushSubscriptionsDb.removeSubscription(endpoint);
|
|
259
|
+
|
|
260
|
+
// Disable webPush in preferences to match subscription state
|
|
261
|
+
const currentPrefs = notificationPreferencesDb.getPreferences(req.user.id);
|
|
262
|
+
if (currentPrefs?.channels?.webPush) {
|
|
263
|
+
notificationPreferencesDb.updatePreferences(req.user.id, {
|
|
264
|
+
...currentPrefs,
|
|
265
|
+
channels: { ...currentPrefs.channels, webPush: false },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
res.json({ success: true });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Error removing push subscription:', error);
|
|
272
|
+
res.status(500).json({ error: 'Failed to remove push subscription' });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
178
276
|
export default router;
|
|
@@ -529,7 +529,7 @@ router.get('/next/:projectName', async (req, res) => {
|
|
|
529
529
|
|
|
530
530
|
// Fallback to loading tasks and finding next one locally
|
|
531
531
|
// Use localhost to bypass proxy for internal server-to-server calls
|
|
532
|
-
const tasksResponse = await fetch(`http://localhost:${process.env.PORT || 3001}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, {
|
|
532
|
+
const tasksResponse = await fetch(`http://localhost:${process.env.SERVER_PORT || process.env.PORT || '3001'}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, {
|
|
533
533
|
headers: {
|
|
534
534
|
'Authorization': req.headers.authorization
|
|
535
535
|
}
|
|
@@ -1960,4 +1960,4 @@ Brief description of what this web application will do and why it's needed.
|
|
|
1960
1960
|
];
|
|
1961
1961
|
}
|
|
1962
1962
|
|
|
1963
|
-
export default router;
|
|
1963
|
+
export default router;
|