buddy-workbench 0.1.86 → 0.1.88
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/package.json +12 -1
- package/server/config.js +2 -0
- package/server/repositories/mind-maps.js +87 -0
- package/server/routes/mind-maps.js +158 -0
- package/server/routes/updates.js +18 -3
- package/server/services/clipboard-history.js +38 -6
- package/server.js +2 -0
- package/ui/dist/assets/index-B1yWhudx.css +1 -0
- package/ui/dist/assets/index-BUVAKtza.js +590 -0
- package/ui/dist/assets/{index-Dty-56mC.js → index-_vMuByUV.js} +3 -3
- package/ui/dist/assets/xmindAdapter-u_vUsbsU.js +16 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-B2G5bFQR.css +0 -1
- package/ui/dist/assets/index-CvDMB0B-.js +0 -586
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.88",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
"version": "node -e \"const v=process.env.npm_package_version; const fs=require('fs'); ['ui/package.json', 'ui/package-lock.json'].forEach(p=>{if(fs.existsSync(p)){const j=JSON.parse(fs.readFileSync(p)); j.version=v; if(j.packages&&j.packages['']){j.packages[''].version=v;} fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');}});\" && git add ui/package.json ui/package-lock.json"
|
|
25
25
|
},
|
|
26
26
|
"devbuddyChangelog": [
|
|
27
|
+
{
|
|
28
|
+
"version": "0.1.87",
|
|
29
|
+
"name": "Mind maps and test case workflow",
|
|
30
|
+
"notes": [
|
|
31
|
+
"Added editable Mind maps list and detail routes with XMind, JSON, and Markdown import/export, plus one JSON data file per map.",
|
|
32
|
+
"Added a left-to-right canvas with centered map positioning, expanded pan space, stable zoom controls, canvas panning, and expand-all/collapse-all actions.",
|
|
33
|
+
"Added Jira issue settings with key or link import, summary lookup, key/summary search, clickable issue cards, node tagging and untagging, and tagged-state styling.",
|
|
34
|
+
"Added node workflow controls for tested status cascading, moving a node subtree with an Ant Design Cascader, and confirmation before deleting maps or ideas.",
|
|
35
|
+
"Refined Mind maps headers, inspector controls, node spacing, Jira presentation, and canvas layout to support test case planning more clearly."
|
|
36
|
+
]
|
|
37
|
+
},
|
|
27
38
|
{
|
|
28
39
|
"version": "0.1.42",
|
|
29
40
|
"name": "Jira Workspace and navigation improvements",
|
package/server/config.js
CHANGED
|
@@ -36,6 +36,8 @@ export const paths = {
|
|
|
36
36
|
branchSync: join(userDataDir, 'branch-sync.json'),
|
|
37
37
|
packageUpgrade: join(userDataDir, 'package-upgrade.json'),
|
|
38
38
|
presentations: join(userDataDir, 'presentations.json'),
|
|
39
|
+
mindMaps: join(userDataDir, 'mind-maps'),
|
|
40
|
+
legacyMindMaps: join(userDataDir, 'mind-maps.json'),
|
|
39
41
|
shutdownLog: join(userDataDir, 'shutdown.log'),
|
|
40
42
|
clipboardDir: join(userDataDir, 'clipboard'),
|
|
41
43
|
plugins: join(root, 'plugins'),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { paths } from '../config.js';
|
|
11
|
+
|
|
12
|
+
const getMapFilename = (id) => `${encodeURIComponent(String(id))}.json`;
|
|
13
|
+
|
|
14
|
+
function ensureMindMapsDir() {
|
|
15
|
+
mkdirSync(paths.mindMaps, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapFiles() {
|
|
19
|
+
if (!existsSync(paths.mindMaps)) return [];
|
|
20
|
+
|
|
21
|
+
return readdirSync(paths.mindMaps, { withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
23
|
+
.map((entry) => entry.name)
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeMindMapFile(map) {
|
|
28
|
+
if (!map?.id) return;
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(paths.mindMaps, getMapFilename(map.id)),
|
|
31
|
+
JSON.stringify(map, null, 2),
|
|
32
|
+
'utf8'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function migrateLegacyMindMaps() {
|
|
37
|
+
if (!existsSync(paths.legacyMindMaps)) return;
|
|
38
|
+
|
|
39
|
+
ensureMindMapsDir();
|
|
40
|
+
const existingFiles = mapFiles();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(paths.legacyMindMaps, 'utf8'));
|
|
44
|
+
if (!existingFiles.length && Array.isArray(parsed)) {
|
|
45
|
+
parsed.forEach(writeMindMapFile);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Remove an invalid legacy file so it cannot block the new storage format.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unlinkSync(paths.legacyMindMaps);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listMindMaps() {
|
|
55
|
+
try {
|
|
56
|
+
migrateLegacyMindMaps();
|
|
57
|
+
return mapFiles().flatMap((filename) => {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(join(paths.mindMaps, filename), 'utf8'));
|
|
60
|
+
return parsed && typeof parsed === 'object' ? [parsed] : [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveMindMaps(mindMaps) {
|
|
71
|
+
migrateLegacyMindMaps();
|
|
72
|
+
ensureMindMapsDir();
|
|
73
|
+
|
|
74
|
+
const expectedFiles = new Set();
|
|
75
|
+
for (const map of Array.isArray(mindMaps) ? mindMaps : []) {
|
|
76
|
+
if (!map?.id) continue;
|
|
77
|
+
const filename = getMapFilename(map.id);
|
|
78
|
+
expectedFiles.add(filename);
|
|
79
|
+
writeMindMapFile(map);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const filename of mapFiles()) {
|
|
83
|
+
if (!expectedFiles.has(filename)) {
|
|
84
|
+
unlinkSync(join(paths.mindMaps, filename));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { readSettings } from '../repositories/settings.js';
|
|
6
|
+
import { listMindMaps, saveMindMaps } from '../repositories/mind-maps.js';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
const httpClient = axios.create({
|
|
10
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
|
11
|
+
validateStatus: () => true,
|
|
12
|
+
timeout: 15000
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const makeId = () => crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
function getJiraHost(domain) {
|
|
18
|
+
const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
19
|
+
if (!clean) return '';
|
|
20
|
+
return clean.includes('jira') || clean.includes('.atlassian.net') ? clean : `jira.${clean}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractJiraKey(value) {
|
|
24
|
+
const match = String(value || '').match(/(?:\/browse\/|\b)([A-Z][A-Z0-9_]*-\d+)\b/i);
|
|
25
|
+
return match?.[1]?.toUpperCase() || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveJiraIssue(value) {
|
|
29
|
+
const input = String(value || '').trim();
|
|
30
|
+
const key = extractJiraKey(input);
|
|
31
|
+
if (!key) throw new Error('Enter a valid Jira No or Jira issue link.');
|
|
32
|
+
|
|
33
|
+
const settings = readSettings();
|
|
34
|
+
const host = getJiraHost(settings.domain);
|
|
35
|
+
if (!host) throw new Error('Jira domain is not configured. Please set Domain in Settings.');
|
|
36
|
+
|
|
37
|
+
const headers = { Accept: 'application/json' };
|
|
38
|
+
if (settings.jiraAccessToken) {
|
|
39
|
+
headers.Authorization = settings.jiraAccessToken.startsWith('Bearer ')
|
|
40
|
+
? settings.jiraAccessToken
|
|
41
|
+
: `Bearer ${settings.jiraAccessToken}`;
|
|
42
|
+
}
|
|
43
|
+
const response = await httpClient.get(
|
|
44
|
+
`https://${host}/rest/api/2/issue/${encodeURIComponent(key)}?fields=summary`,
|
|
45
|
+
{ headers }
|
|
46
|
+
);
|
|
47
|
+
if (response.status < 200 || response.status >= 300) {
|
|
48
|
+
const detail = response.data?.errorMessages?.[0] || response.data?.message || response.statusText;
|
|
49
|
+
throw new Error(detail || `Jira returned status ${response.status}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const summary = String(response.data?.fields?.summary || '').trim();
|
|
53
|
+
if (!summary) throw new Error(`Jira issue ${key} has no summary.`);
|
|
54
|
+
return {
|
|
55
|
+
id: String(response.data?.id || key),
|
|
56
|
+
key: String(response.data?.key || key).toUpperCase(),
|
|
57
|
+
summary,
|
|
58
|
+
url: /^https?:\/\//i.test(input) ? input : `https://${host}/browse/${encodeURIComponent(key)}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeNode(node, fallbackText = 'Untitled idea') {
|
|
63
|
+
const text = String(node?.text ?? node?.label ?? fallbackText).trim() || fallbackText;
|
|
64
|
+
const x = Number(node?.position?.x);
|
|
65
|
+
const y = Number(node?.position?.y);
|
|
66
|
+
return {
|
|
67
|
+
id: String(node?.id || makeId()),
|
|
68
|
+
text,
|
|
69
|
+
...(typeof node?.color === 'string' && node.color.trim() ? { color: node.color.trim() } : {}),
|
|
70
|
+
collapsed: Boolean(node?.collapsed),
|
|
71
|
+
tested: Boolean(node?.tested),
|
|
72
|
+
...(typeof node?.notes === 'string' && node.notes ? { notes: node.notes } : {}),
|
|
73
|
+
...(Array.isArray(node?.jiraKeys)
|
|
74
|
+
? { jiraKeys: [...new Set(node.jiraKeys.map((key) => String(key).trim().toUpperCase()).filter(Boolean))] }
|
|
75
|
+
: {}),
|
|
76
|
+
...(Number.isFinite(x) && Number.isFinite(y) ? { position: { x, y } } : {}),
|
|
77
|
+
children: Array.isArray(node?.children)
|
|
78
|
+
? node.children.map((child) => normalizeNode(child))
|
|
79
|
+
: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeMindMap(input, { id = makeId(), createdAt = new Date().toISOString() } = {}) {
|
|
84
|
+
const rootInput = input?.root || input?.data?.root;
|
|
85
|
+
if (!rootInput || typeof rootInput !== 'object') return null;
|
|
86
|
+
const title = String(input?.title || rootInput.text || 'Untitled mind map').trim() || 'Untitled mind map';
|
|
87
|
+
const root = normalizeNode(rootInput, title);
|
|
88
|
+
return {
|
|
89
|
+
id: String(id),
|
|
90
|
+
title,
|
|
91
|
+
layout: 'left-to-right',
|
|
92
|
+
createdAt,
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
jiraIssues: Array.isArray(input?.jiraIssues)
|
|
95
|
+
? input.jiraIssues
|
|
96
|
+
.map((issue) => ({
|
|
97
|
+
id: String(issue?.id || issue?.key || '').trim(),
|
|
98
|
+
key: String(issue?.key || '').trim().toUpperCase(),
|
|
99
|
+
summary: String(issue?.summary || '').trim(),
|
|
100
|
+
url: typeof issue?.url === 'string' ? issue.url.trim() : ''
|
|
101
|
+
}))
|
|
102
|
+
.filter((issue) => issue.id && issue.key)
|
|
103
|
+
: [],
|
|
104
|
+
root: { ...root, text: root.text || title }
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
router.get('/', (_req, res) => {
|
|
109
|
+
res.json(listMindMaps());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
router.get('/:id', (req, res) => {
|
|
113
|
+
const mindMap = listMindMaps().find((item) => String(item.id) === String(req.params.id));
|
|
114
|
+
if (!mindMap) return res.status(404).json({ error: 'Mind map not found.' });
|
|
115
|
+
res.json(mindMap);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
router.post('/jira-issues/resolve', async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
res.json(await resolveJiraIssue(req.body?.input));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
res.status(400).json({ error: error.message || 'Unable to load Jira issue summary.' });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.post('/', (req, res) => {
|
|
127
|
+
const mindMap = normalizeMindMap(req.body);
|
|
128
|
+
if (!mindMap) return res.status(400).json({ error: 'A root node is required.' });
|
|
129
|
+
const mindMaps = listMindMaps();
|
|
130
|
+
mindMaps.unshift(mindMap);
|
|
131
|
+
saveMindMaps(mindMaps);
|
|
132
|
+
res.status(201).json(mindMap);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
router.put('/:id', (req, res) => {
|
|
136
|
+
const mindMaps = listMindMaps();
|
|
137
|
+
const index = mindMaps.findIndex((item) => String(item.id) === String(req.params.id));
|
|
138
|
+
if (index < 0) return res.status(404).json({ error: 'Mind map not found.' });
|
|
139
|
+
|
|
140
|
+
const updated = normalizeMindMap(req.body, {
|
|
141
|
+
id: mindMaps[index].id,
|
|
142
|
+
createdAt: mindMaps[index].createdAt || new Date().toISOString()
|
|
143
|
+
});
|
|
144
|
+
if (!updated) return res.status(400).json({ error: 'A root node is required.' });
|
|
145
|
+
mindMaps[index] = updated;
|
|
146
|
+
saveMindMaps(mindMaps);
|
|
147
|
+
res.json(updated);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
router.delete('/:id', (req, res) => {
|
|
151
|
+
const mindMaps = listMindMaps();
|
|
152
|
+
const next = mindMaps.filter((item) => String(item.id) !== String(req.params.id));
|
|
153
|
+
if (next.length === mindMaps.length) return res.status(404).json({ error: 'Mind map not found.' });
|
|
154
|
+
saveMindMaps(next);
|
|
155
|
+
res.status(204).end();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export default router;
|
package/server/routes/updates.js
CHANGED
|
@@ -139,13 +139,17 @@ async function readNpmPackageMetadata() {
|
|
|
139
139
|
|
|
140
140
|
async function checkForUpdates() {
|
|
141
141
|
const metadata = await readNpmPackageMetadata();
|
|
142
|
-
const
|
|
142
|
+
const releases = buildReleaseList(metadata);
|
|
143
|
+
// A release is only visible after the 48-hour safety window. Keep the
|
|
144
|
+
// summary and the release list on the same source of truth so an update
|
|
145
|
+
// can never target a newer, still-hidden npm release.
|
|
146
|
+
const latestVersion = releases[0]?.version || CURRENT_VERSION;
|
|
143
147
|
return {
|
|
144
148
|
currentVersion: CURRENT_VERSION,
|
|
145
149
|
latestVersion,
|
|
146
150
|
updateAvailable: compareVersions(latestVersion, CURRENT_VERSION) > 0,
|
|
147
151
|
checkedAt: new Date().toISOString(),
|
|
148
|
-
releases
|
|
152
|
+
releases,
|
|
149
153
|
sourceUnavailable: false
|
|
150
154
|
};
|
|
151
155
|
}
|
|
@@ -215,7 +219,18 @@ router.post('/self-update', async (req, res) => {
|
|
|
215
219
|
|
|
216
220
|
selfUpdateRunning = true;
|
|
217
221
|
try {
|
|
218
|
-
|
|
222
|
+
let version = normalizeUpdateVersion(req.body?.version);
|
|
223
|
+
if (version === 'latest') {
|
|
224
|
+
const status = cachedResult && !cachedResult.sourceUnavailable && Date.now() - cachedAt < CACHE_TTL_MS
|
|
225
|
+
? cachedResult
|
|
226
|
+
: await checkForUpdates();
|
|
227
|
+
version = status.latestVersion;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (compareVersions(version, CURRENT_VERSION) <= 0) {
|
|
231
|
+
return res.json({ success: true, message: `DevBuddy is already up to date (${CURRENT_VERSION}).` });
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
if (process.platform === 'win32') {
|
|
220
235
|
scheduleWindowsSelfUpdate(`${PACKAGE_NAME}@${version}`);
|
|
221
236
|
cachedResult = null;
|
|
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import encodeMozjpeg, { init as initMozjpeg } from '@jsquash/jpeg/encode.js';
|
|
8
8
|
import { paths } from '../config.js';
|
|
9
|
+
import { addErrorRecord } from '../repositories/errors.js';
|
|
9
10
|
import { readSettings } from '../repositories/settings.js';
|
|
10
11
|
import { readWindowsClipboard } from './windows.js';
|
|
11
12
|
|
|
@@ -15,6 +16,22 @@ let lastValue = '';
|
|
|
15
16
|
let lastImageHash = '';
|
|
16
17
|
let suppressImageCaptureUntil = 0;
|
|
17
18
|
let mozjpegWasmModule = null;
|
|
19
|
+
const clipboardErrorLogTimes = new Map();
|
|
20
|
+
|
|
21
|
+
function logClipboardError(stage, error) {
|
|
22
|
+
// Clipboard polling runs continuously. Avoid filling Error Log with the
|
|
23
|
+
// same native-tool failure every two seconds while retaining diagnostics.
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const lastLoggedAt = clipboardErrorLogTimes.get(stage) || 0;
|
|
26
|
+
if (now - lastLoggedAt < 60 * 1000) return;
|
|
27
|
+
clipboardErrorLogTimes.set(stage, now);
|
|
28
|
+
const details = String(error?.stderr || error?.stdout || error?.stack || error?.message || error || '').trim();
|
|
29
|
+
addErrorRecord({
|
|
30
|
+
source: 'Clipboard Monitor',
|
|
31
|
+
message: `Failed during ${stage}.`,
|
|
32
|
+
details: details || 'The native clipboard operation returned an unknown error.'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
18
35
|
|
|
19
36
|
async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
|
|
20
37
|
if (!mozjpegWasmModule) {
|
|
@@ -72,7 +89,8 @@ async function getMacClipboardImageData() {
|
|
|
72
89
|
const base64 = str.slice(secondColon + 1);
|
|
73
90
|
if (!width || !height || !base64) return null;
|
|
74
91
|
return { width, height, rgbaBuf: Buffer.from(base64, 'base64') };
|
|
75
|
-
} catch {
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logClipboardError('macOS image clipboard read (osascript)', error);
|
|
76
94
|
return null;
|
|
77
95
|
}
|
|
78
96
|
}
|
|
@@ -95,7 +113,8 @@ if let data = pb.data(forType: .png) {
|
|
|
95
113
|
try {
|
|
96
114
|
const { stdout } = await execFileAsync('swift', ['-e', swiftCode], { encoding: 'buffer', timeout: 3000 });
|
|
97
115
|
if (stdout && stdout.length > 0) return stdout;
|
|
98
|
-
} catch {
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logClipboardError('macOS image clipboard read (Swift)', error);
|
|
99
118
|
try {
|
|
100
119
|
const jsaScript = `
|
|
101
120
|
ObjC.import("AppKit");
|
|
@@ -118,7 +137,9 @@ if let data = pb.data(forType: .png) {
|
|
|
118
137
|
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', jsaScript], { timeout: 3000 });
|
|
119
138
|
const str = stdout.trim();
|
|
120
139
|
if (str) return Buffer.from(str, 'base64');
|
|
121
|
-
} catch {
|
|
140
|
+
} catch (fallbackError) {
|
|
141
|
+
logClipboardError('macOS image clipboard fallback read (osascript)', fallbackError);
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
144
|
return null;
|
|
124
145
|
}
|
|
@@ -330,9 +351,16 @@ export async function captureClipboard() {
|
|
|
330
351
|
try {
|
|
331
352
|
const mozjpegBuf = await compressMozjpeg(imageData.rgbaBuf, imageData.width, imageData.height, 75);
|
|
332
353
|
saveClipboardImage(mozjpegBuf, 'jpg', hash);
|
|
333
|
-
} catch {
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logClipboardError('image compression or save (WASM/JPEG)', error);
|
|
334
356
|
const rawBuf = await getMacClipboardImageBuffer();
|
|
335
|
-
if (rawBuf)
|
|
357
|
+
if (rawBuf) {
|
|
358
|
+
try {
|
|
359
|
+
saveClipboardImage(rawBuf, 'png', hash);
|
|
360
|
+
} catch (fallbackError) {
|
|
361
|
+
logClipboardError('raw image save after compression fallback', fallbackError);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
336
364
|
}
|
|
337
365
|
}
|
|
338
366
|
}
|
|
@@ -357,7 +385,11 @@ export async function captureClipboard() {
|
|
|
357
385
|
|
|
358
386
|
lastImageHash = hash;
|
|
359
387
|
if (!isDuplicate) {
|
|
360
|
-
|
|
388
|
+
try {
|
|
389
|
+
saveClipboardImage(imageBuffer, 'png', hash);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
logClipboardError('image save', error);
|
|
392
|
+
}
|
|
361
393
|
}
|
|
362
394
|
}
|
|
363
395
|
}
|
package/server.js
CHANGED
|
@@ -26,6 +26,7 @@ import branchSyncRoutes from './server/routes/branch-sync.js';
|
|
|
26
26
|
import updateRoutes from './server/routes/updates.js';
|
|
27
27
|
import dataBackupRoutes from './server/routes/data-backup.js';
|
|
28
28
|
import presentationsRoutes from './server/routes/presentations.js';
|
|
29
|
+
import mindMapsRoutes from './server/routes/mind-maps.js';
|
|
29
30
|
import packageUpgradeRoutes from './server/routes/package-upgrade.js';
|
|
30
31
|
import overviewRoutes from './server/routes/overview.js';
|
|
31
32
|
import { addErrorRecord } from './server/repositories/errors.js';
|
|
@@ -103,6 +104,7 @@ app.use('/api/branch-sync', branchSyncRoutes);
|
|
|
103
104
|
app.use('/api/updates', updateRoutes);
|
|
104
105
|
app.use('/api/data-backup', dataBackupRoutes);
|
|
105
106
|
app.use('/api/presentations', presentationsRoutes);
|
|
107
|
+
app.use('/api/mind-maps', mindMapsRoutes);
|
|
106
108
|
app.use('/api/package-upgrade', packageUpgradeRoutes);
|
|
107
109
|
app.use('/api/overview', overviewRoutes);
|
|
108
110
|
|