Skip to content

Commit 96f273c

Browse files
authored
Merge pull request #133 from atomantic/better/code-quality
fix: lazy-load ChiefOfStaff, async pathExists, accessibility labels, hook stability
2 parents aec3710 + 871bee8 commit 96f273c

15 files changed

Lines changed: 348 additions & 434 deletions

File tree

client/src/components/digital-twin/tabs/EnrichTab.jsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function EnrichTab({ onRefresh }) {
4545

4646
const loadData = useCallback(async () => {
4747
setLoading(true);
48-
const progressData = await api.getSoulEnrichProgress().catch(() => null);
48+
const progressData = await api.getDigitalTwinEnrichProgress().catch(() => null);
4949
setProgress(progressData);
5050
setLoading(false);
5151
}, []);
@@ -114,34 +114,39 @@ export default function EnrichTab({ onRefresh }) {
114114
}
115115

116116
setSavingWritingStyle(true);
117-
await api.createSoulDocument({
118-
filename: 'WRITING_STYLE.md',
119-
title: 'Writing Style',
120-
category: 'core',
121-
content: writingAnalysis.suggestedContent
122-
}).catch(async () => {
123-
// Document might exist, try to update by fetching ID
124-
const docs = await api.getSoulDocuments();
125-
const existing = docs.find(d => d.filename === 'WRITING_STYLE.md');
126-
if (existing) {
127-
return api.updateSoulDocument(existing.id, {
128-
content: writingAnalysis.suggestedContent
129-
});
130-
}
131-
});
132-
toast.success('Writing style saved');
117+
try {
118+
await api.createSoulDocument({
119+
filename: 'WRITING_STYLE.md',
120+
title: 'Writing Style',
121+
category: 'core',
122+
content: writingAnalysis.suggestedContent
123+
}).catch(async () => {
124+
// Document might exist, try to update by fetching ID
125+
const docs = await api.getDigitalTwinDocuments();
126+
const existing = docs.find(d => d.filename === 'WRITING_STYLE.md');
127+
if (existing) {
128+
return api.updateSoulDocument(existing.id, {
129+
content: writingAnalysis.suggestedContent
130+
});
131+
}
132+
});
133+
toast.success('Writing style saved');
134+
} catch (err) {
135+
toast.error(`Failed to save writing style: ${err.message}`);
136+
}
133137
setSavingWritingStyle(false);
134138
onRefresh();
135139
};
136140

137141
const loadQuestion = useCallback(async (categoryId, skipList = []) => {
138142
setLoadingQuestion(true);
139143
try {
140-
const question = await api.getSoulEnrichQuestion(categoryId, undefined, undefined, skipList.length ? skipList : undefined);
144+
const question = await api.getDigitalTwinEnrichQuestion(categoryId, undefined, undefined, skipList.length ? skipList : undefined);
141145
setCurrentQuestion(question);
142146
setAnswer('');
143147
setScaleValue(null);
144-
} catch {
148+
} catch (err) {
149+
console.warn(`⚠️ Failed to load enrichment question: ${err.message}`);
145150
setCurrentQuestion(null);
146151
} finally {
147152
setLoadingQuestion(false);

client/src/hooks/useAutoRefetch.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ export function useAutoRefetch(fetchFn, intervalMs) {
2727
if (cancelled) return;
2828
setData(result);
2929
setLoading(false);
30-
} catch {
30+
} catch (err) {
3131
// Keep prior data on failure, just clear loading state
32+
console.warn(`⚠️ Auto-refetch failed: ${err.message}`);
3233
if (!cancelled) setLoading(false);
3334
}
3435
};

client/src/pages/CharacterSheet.jsx

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,12 @@ import {
55
} from 'lucide-react';
66
import toast from '../components/ui/Toast';
77
import { timeAgo } from '../utils/formatters';
8-
import { generateAvatar } from '../services/api';
8+
import api, { generateAvatar } from '../services/api';
99
import socket from '../services/socket';
1010

11-
const request = async (endpoint, options = {}) => {
12-
const response = await fetch(`/api/character${endpoint}`, {
13-
headers: { 'Content-Type': 'application/json', ...options.headers },
14-
...options
15-
});
16-
let data = null;
17-
try { data = await response.json(); } catch { /* non-JSON body */ }
18-
if (!response.ok) {
19-
const message = data?.message || `Request failed with status ${response.status}`;
20-
const error = new Error(message);
21-
error.status = response.status;
22-
throw error;
23-
}
24-
return data;
25-
};
26-
27-
const get = () => request('');
28-
const post = (path, body) => request(path, { method: 'POST', body: JSON.stringify(body) });
29-
const put = (body) => request('', { method: 'PUT', body: JSON.stringify(body) });
11+
const charGet = () => api.get('/character');
12+
const charPost = (path, body) => api.post(`/character${path}`, body);
13+
const charPut = (body) => api.put('/character', body);
3014

3115
// D&D 5e XP thresholds (must match server)
3216
const XP_THRESHOLDS = [
@@ -91,7 +75,7 @@ export default function CharacterSheet() {
9175
const load = useCallback(async () => {
9276
setLoadError(null);
9377
try {
94-
const data = await get();
78+
const data = await charGet();
9579
if (!data || data.error) {
9680
setLoadError('Failed to load character data');
9781
return;
@@ -143,7 +127,7 @@ export default function CharacterSheet() {
143127

144128
const handleDamage = async () => {
145129
try {
146-
const result = await post('/damage', { diceNotation: dmgDice, description: dmgDesc || undefined });
130+
const result = await charPost('/damage', { diceNotation: dmgDice, description: dmgDesc || undefined });
147131
setChar(result.character);
148132
setActiveAction(null);
149133
setDmgDice('1d6');
@@ -153,22 +137,22 @@ export default function CharacterSheet() {
153137

154138
const handleShortRest = async () => {
155139
try {
156-
const result = await post('/rest', { type: 'short' });
140+
const result = await charPost('/rest', { type: 'short' });
157141
setChar(result.character);
158142
} catch (err) { toast.error(err.message || 'Failed to take short rest'); }
159143
};
160144

161145
const handleLongRest = async () => {
162146
try {
163-
const result = await post('/rest', { type: 'long' });
147+
const result = await charPost('/rest', { type: 'long' });
164148
setChar(result.character);
165149
} catch (err) { toast.error(err.message || 'Failed to take long rest'); }
166150
};
167151

168152
const handleAddXp = async () => {
169153
if (!xpAmount) return;
170154
try {
171-
const result = await post('/xp', { amount: Number(xpAmount), source: 'manual', description: xpDesc || undefined });
155+
const result = await charPost('/xp', { amount: Number(xpAmount), source: 'manual', description: xpDesc || undefined });
172156
setChar(result.character);
173157
setActiveAction(null);
174158
setXpAmount('');
@@ -182,7 +166,7 @@ export default function CharacterSheet() {
182166
const body = { description: evtDesc };
183167
if (evtXp) body.xp = Number(evtXp);
184168
if (evtDice) body.diceNotation = evtDice;
185-
const result = await post('/event', body);
169+
const result = await charPost('/event', body);
186170
setChar(result.character);
187171
setActiveAction(null);
188172
setEvtDesc('');
@@ -194,7 +178,7 @@ export default function CharacterSheet() {
194178
const handleSync = async (type) => {
195179
setSyncing(type);
196180
try {
197-
const result = await post(`/sync/${type}`, {});
181+
const result = await charPost(`/sync/${type}`, {});
198182
setChar(result.character);
199183
} catch (err) {
200184
toast.error(err.message || `Failed to sync ${type}`);
@@ -206,7 +190,7 @@ export default function CharacterSheet() {
206190
const handleNameSave = async () => {
207191
try {
208192
if (nameVal.trim() && nameVal !== char.name) {
209-
const data = await put({ name: nameVal.trim() });
193+
const data = await charPut({ name: nameVal.trim() });
210194
setChar(data);
211195
}
212196
} catch (err) { toast.error(err.message || 'Failed to save name'); }
@@ -216,7 +200,7 @@ export default function CharacterSheet() {
216200
const handleClassSave = async () => {
217201
try {
218202
if (classVal.trim() && classVal !== char.class) {
219-
const data = await put({ class: classVal.trim() });
203+
const data = await charPut({ class: classVal.trim() });
220204
setChar(data);
221205
}
222206
} catch (err) { toast.error(err.message || 'Failed to save class'); }

client/src/pages/DataDog.jsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ export default function DataDog() {
100100
...(formData.appKey && { appKey: formData.appKey })
101101
};
102102

103-
await api.post('/datadog/instances', payload);
103+
const saved = await api.post('/datadog/instances', payload);
104104

105105
toast.success(`DataDog instance "${payload.name}" saved successfully`);
106-
await loadInstances();
106+
setInstances(prev => ({ ...prev, [saved.id]: saved }));
107107
handleCancel();
108108
} catch (error) {
109109
console.error(`Failed to save DataDog instance: ${error.message}`);
@@ -125,7 +125,11 @@ export default function DataDog() {
125125
try {
126126
await api.delete(`/datadog/instances/${instanceId}`);
127127
toast.success(`DataDog instance "${instanceId}" deleted`);
128-
await loadInstances();
128+
setInstances(prev => {
129+
const next = { ...prev };
130+
delete next[instanceId];
131+
return next;
132+
});
129133
} catch (error) {
130134
console.error(`Failed to delete DataDog instance: ${error.message}`);
131135
toast.error(`Failed to delete: ${error.message}`);

client/src/pages/FeatureAgentDetail.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,19 @@ export default function FeatureAgentDetail() {
4646
}, [fetchAgent, id]);
4747

4848
const handleStart = useCallback((agentId) => {
49-
api.startFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Activated'); }).catch(() => {});
49+
api.startFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Activated'); }).catch(err => toast.error(err.message || 'Action failed'));
5050
}, []);
5151
const handlePause = useCallback((agentId) => {
52-
api.pauseFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Paused'); }).catch(() => {});
52+
api.pauseFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Paused'); }).catch(err => toast.error(err.message || 'Action failed'));
5353
}, []);
5454
const handleResume = useCallback((agentId) => {
55-
api.resumeFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Resumed'); }).catch(() => {});
55+
api.resumeFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Resumed'); }).catch(err => toast.error(err.message || 'Action failed'));
5656
}, []);
5757
const handleStop = useCallback((agentId) => {
58-
api.stopFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Stopped'); }).catch(() => {});
58+
api.stopFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Stopped'); }).catch(err => toast.error(err.message || 'Action failed'));
5959
}, []);
6060
const handleTrigger = useCallback((agentId) => {
61-
api.triggerFeatureAgent(agentId).then(() => toast.success('Run triggered')).catch(() => {});
61+
api.triggerFeatureAgent(agentId).then(() => toast.success('Run triggered')).catch(err => toast.error(err.message || 'Action failed'));
6262
}, []);
6363

6464
const handleSave = useCallback((saved) => {

client/src/pages/FeatureAgents.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,41 @@ export default function FeatureAgents() {
3333
api.startFeatureAgent(id).then(agent => {
3434
setAgents(prev => prev.map(a => a.id === id ? { ...a, ...agent } : a));
3535
toast.success('Feature agent activated');
36-
}).catch(() => {});
36+
}).catch(err => toast.error(err.message || 'Action failed'));
3737
}, []);
3838

3939
const handlePause = useCallback((id) => {
4040
api.pauseFeatureAgent(id).then(agent => {
4141
setAgents(prev => prev.map(a => a.id === id ? { ...a, ...agent } : a));
4242
toast.success('Feature agent paused');
43-
}).catch(() => {});
43+
}).catch(err => toast.error(err.message || 'Action failed'));
4444
}, []);
4545

4646
const handleResume = useCallback((id) => {
4747
api.resumeFeatureAgent(id).then(agent => {
4848
setAgents(prev => prev.map(a => a.id === id ? { ...a, ...agent } : a));
4949
toast.success('Feature agent resumed');
50-
}).catch(() => {});
50+
}).catch(err => toast.error(err.message || 'Action failed'));
5151
}, []);
5252

5353
const handleStop = useCallback((id) => {
5454
api.stopFeatureAgent(id).then(agent => {
5555
setAgents(prev => prev.map(a => a.id === id ? { ...a, ...agent } : a));
5656
toast.success('Feature agent stopped');
57-
}).catch(() => {});
57+
}).catch(err => toast.error(err.message || 'Action failed'));
5858
}, []);
5959

6060
const handleTrigger = useCallback((id) => {
6161
api.triggerFeatureAgent(id).then(() => {
6262
toast.success('Run triggered');
63-
}).catch(() => {});
63+
}).catch(err => toast.error(err.message || 'Action failed'));
6464
}, []);
6565

6666
const handleDelete = useCallback((id) => {
6767
api.deleteFeatureAgent(id).then(() => {
6868
setAgents(prev => prev.filter(a => a.id !== id));
6969
toast.success('Feature agent deleted');
70-
}).catch(() => {});
70+
}).catch(err => toast.error(err.message || 'Action failed'));
7171
}, []);
7272

7373
const activeCount = agents.filter(a => a.status === 'active').length;

client/src/pages/Jira.jsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Jira() {
4343
const response = await api.get('/jira/instances');
4444
setInstances(response.instances || {});
4545
} catch (error) {
46-
console.error('Failed to load JIRA instances:', error);
46+
console.error(`Failed to load JIRA instances: ${error.message}`);
4747
toast.error(`Failed to load JIRA instances: ${error.message}`);
4848
} finally {
4949
setLoading(false);
@@ -102,13 +102,13 @@ export default function Jira() {
102102
apiToken: formData.apiToken
103103
};
104104

105-
await api.post('/jira/instances', payload);
105+
const saved = await api.post('/jira/instances', payload);
106106

107107
toast.success(`JIRA instance "${payload.name}" saved successfully`);
108-
await loadInstances();
108+
setInstances(prev => ({ ...prev, [saved.id]: saved }));
109109
handleCancel();
110110
} catch (error) {
111-
console.error('Failed to save JIRA instance:', error);
111+
console.error(`Failed to save JIRA instance: ${error.message}`);
112112
setSaveError(error.message);
113113
toast.error(`Failed to save: ${error.message}`);
114114
} finally {
@@ -127,9 +127,13 @@ export default function Jira() {
127127
try {
128128
await api.delete(`/jira/instances/${instanceId}`);
129129
toast.success(`JIRA instance "${instanceId}" deleted`);
130-
await loadInstances();
130+
setInstances(prev => {
131+
const next = { ...prev };
132+
delete next[instanceId];
133+
return next;
134+
});
131135
} catch (error) {
132-
console.error('Failed to delete JIRA instance:', error);
136+
console.error(`Failed to delete JIRA instance: ${error.message}`);
133137
toast.error(`Failed to delete: ${error.message}`);
134138
}
135139
};

server/lib/telegramClient.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { EventEmitter } from 'events';
88
const BASE_URL = 'https://api.telegram.org/bot';
99
const POLL_TIMEOUT_SEC = 30;
1010
const API_TIMEOUT_MS = 10_000; // regular calls: sendMessage, editMessageText, etc.
11+
const RETRY_DELAY_API_ERROR_MS = 5_000;
12+
const RETRY_DELAY_NETWORK_ERROR_MS = 2_000;
1113

1214
function buildUrl(token, method) {
1315
return `${BASE_URL}${token}/${method}`;
@@ -57,14 +59,14 @@ export function createTelegramBot(token, opts = {}) {
5759
const json = await res.json();
5860
if (!json.ok) {
5961
// Retry after a delay on API errors
60-
await new Promise(r => setTimeout(r, 5000));
62+
await new Promise(r => setTimeout(r, RETRY_DELAY_API_ERROR_MS));
6163
continue;
6264
}
6365
updates = json.result;
6466
} catch {
6567
// Aborted (stopPolling called) or network error
6668
if (!polling) break;
67-
await new Promise(r => setTimeout(r, 2000));
69+
await new Promise(r => setTimeout(r, RETRY_DELAY_NETWORK_ERROR_MS));
6870
continue;
6971
}
7072

0 commit comments

Comments
 (0)