Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions client/src/components/digital-twin/tabs/EnrichTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function EnrichTab({ onRefresh }) {

const loadData = useCallback(async () => {
setLoading(true);
const progressData = await api.getSoulEnrichProgress().catch(() => null);
const progressData = await api.getDigitalTwinEnrichProgress().catch(() => null);
setProgress(progressData);
setLoading(false);
}, []);
Expand Down Expand Up @@ -114,34 +114,39 @@ export default function EnrichTab({ onRefresh }) {
}

setSavingWritingStyle(true);
await api.createSoulDocument({
filename: 'WRITING_STYLE.md',
title: 'Writing Style',
category: 'core',
content: writingAnalysis.suggestedContent
}).catch(async () => {
// Document might exist, try to update by fetching ID
const docs = await api.getSoulDocuments();
const existing = docs.find(d => d.filename === 'WRITING_STYLE.md');
if (existing) {
return api.updateSoulDocument(existing.id, {
content: writingAnalysis.suggestedContent
});
}
});
toast.success('Writing style saved');
try {
await api.createSoulDocument({
filename: 'WRITING_STYLE.md',
title: 'Writing Style',
category: 'core',
content: writingAnalysis.suggestedContent
}).catch(async () => {
// Document might exist, try to update by fetching ID
const docs = await api.getDigitalTwinDocuments();
const existing = docs.find(d => d.filename === 'WRITING_STYLE.md');
if (existing) {
return api.updateSoulDocument(existing.id, {
content: writingAnalysis.suggestedContent
});
}
});
toast.success('Writing style saved');
} catch (err) {
toast.error(`Failed to save writing style: ${err.message}`);
}
setSavingWritingStyle(false);
onRefresh();
};

const loadQuestion = useCallback(async (categoryId, skipList = []) => {
setLoadingQuestion(true);
try {
const question = await api.getSoulEnrichQuestion(categoryId, undefined, undefined, skipList.length ? skipList : undefined);
const question = await api.getDigitalTwinEnrichQuestion(categoryId, undefined, undefined, skipList.length ? skipList : undefined);
setCurrentQuestion(question);
setAnswer('');
setScaleValue(null);
} catch {
} catch (err) {
console.warn(`⚠️ Failed to load enrichment question: ${err.message}`);
setCurrentQuestion(null);
} finally {
setLoadingQuestion(false);
Expand Down
3 changes: 2 additions & 1 deletion client/src/hooks/useAutoRefetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export function useAutoRefetch(fetchFn, intervalMs) {
if (cancelled) return;
setData(result);
setLoading(false);
} catch {
} catch (err) {
// Keep prior data on failure, just clear loading state
console.warn(`⚠️ Auto-refetch failed: ${err.message}`);
if (!cancelled) setLoading(false);
}
};
Expand Down
42 changes: 13 additions & 29 deletions client/src/pages/CharacterSheet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,12 @@ import {
} from 'lucide-react';
import toast from '../components/ui/Toast';
import { timeAgo } from '../utils/formatters';
import { generateAvatar } from '../services/api';
import api, { generateAvatar } from '../services/api';
import socket from '../services/socket';

const request = async (endpoint, options = {}) => {
const response = await fetch(`/api/character${endpoint}`, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
});
let data = null;
try { data = await response.json(); } catch { /* non-JSON body */ }
if (!response.ok) {
const message = data?.message || `Request failed with status ${response.status}`;
const error = new Error(message);
error.status = response.status;
throw error;
}
return data;
};

const get = () => request('');
const post = (path, body) => request(path, { method: 'POST', body: JSON.stringify(body) });
const put = (body) => request('', { method: 'PUT', body: JSON.stringify(body) });
const charGet = () => api.get('/character');
const charPost = (path, body) => api.post(`/character${path}`, body);
const charPut = (body) => api.put('/character', body);

// D&D 5e XP thresholds (must match server)
const XP_THRESHOLDS = [
Expand Down Expand Up @@ -91,7 +75,7 @@ export default function CharacterSheet() {
const load = useCallback(async () => {
setLoadError(null);
try {
const data = await get();
const data = await charGet();
if (!data || data.error) {
setLoadError('Failed to load character data');
return;
Expand Down Expand Up @@ -143,7 +127,7 @@ export default function CharacterSheet() {

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

const handleShortRest = async () => {
try {
const result = await post('/rest', { type: 'short' });
const result = await charPost('/rest', { type: 'short' });
setChar(result.character);
} catch (err) { toast.error(err.message || 'Failed to take short rest'); }
};

const handleLongRest = async () => {
try {
const result = await post('/rest', { type: 'long' });
const result = await charPost('/rest', { type: 'long' });
setChar(result.character);
} catch (err) { toast.error(err.message || 'Failed to take long rest'); }
};

const handleAddXp = async () => {
if (!xpAmount) return;
try {
const result = await post('/xp', { amount: Number(xpAmount), source: 'manual', description: xpDesc || undefined });
const result = await charPost('/xp', { amount: Number(xpAmount), source: 'manual', description: xpDesc || undefined });
setChar(result.character);
setActiveAction(null);
setXpAmount('');
Expand All @@ -182,7 +166,7 @@ export default function CharacterSheet() {
const body = { description: evtDesc };
if (evtXp) body.xp = Number(evtXp);
if (evtDice) body.diceNotation = evtDice;
const result = await post('/event', body);
const result = await charPost('/event', body);
setChar(result.character);
setActiveAction(null);
setEvtDesc('');
Expand All @@ -194,7 +178,7 @@ export default function CharacterSheet() {
const handleSync = async (type) => {
setSyncing(type);
try {
const result = await post(`/sync/${type}`, {});
const result = await charPost(`/sync/${type}`, {});
setChar(result.character);
} catch (err) {
toast.error(err.message || `Failed to sync ${type}`);
Expand All @@ -206,7 +190,7 @@ export default function CharacterSheet() {
const handleNameSave = async () => {
try {
if (nameVal.trim() && nameVal !== char.name) {
const data = await put({ name: nameVal.trim() });
const data = await charPut({ name: nameVal.trim() });
setChar(data);
}
} catch (err) { toast.error(err.message || 'Failed to save name'); }
Expand All @@ -216,7 +200,7 @@ export default function CharacterSheet() {
const handleClassSave = async () => {
try {
if (classVal.trim() && classVal !== char.class) {
const data = await put({ class: classVal.trim() });
const data = await charPut({ class: classVal.trim() });
setChar(data);
}
} catch (err) { toast.error(err.message || 'Failed to save class'); }
Expand Down
10 changes: 7 additions & 3 deletions client/src/pages/DataDog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ export default function DataDog() {
...(formData.appKey && { appKey: formData.appKey })
};

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

toast.success(`DataDog instance "${payload.name}" saved successfully`);
await loadInstances();
setInstances(prev => ({ ...prev, [saved.id]: saved }));
handleCancel();
} catch (error) {
console.error(`Failed to save DataDog instance: ${error.message}`);
Expand All @@ -125,7 +125,11 @@ export default function DataDog() {
try {
await api.delete(`/datadog/instances/${instanceId}`);
toast.success(`DataDog instance "${instanceId}" deleted`);
await loadInstances();
setInstances(prev => {
const next = { ...prev };
delete next[instanceId];
return next;
});
} catch (error) {
console.error(`Failed to delete DataDog instance: ${error.message}`);
toast.error(`Failed to delete: ${error.message}`);
Expand Down
10 changes: 5 additions & 5 deletions client/src/pages/FeatureAgentDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ export default function FeatureAgentDetail() {
}, [fetchAgent, id]);

const handleStart = useCallback((agentId) => {
api.startFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Activated'); }).catch(() => {});
api.startFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Activated'); }).catch(err => toast.error(err.message || 'Action failed'));
}, []);
const handlePause = useCallback((agentId) => {
api.pauseFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Paused'); }).catch(() => {});
api.pauseFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Paused'); }).catch(err => toast.error(err.message || 'Action failed'));
}, []);
const handleResume = useCallback((agentId) => {
api.resumeFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Resumed'); }).catch(() => {});
api.resumeFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Resumed'); }).catch(err => toast.error(err.message || 'Action failed'));
}, []);
const handleStop = useCallback((agentId) => {
api.stopFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Stopped'); }).catch(() => {});
api.stopFeatureAgent(agentId).then(data => { setAgent(prev => ({ ...prev, ...data })); toast.success('Stopped'); }).catch(err => toast.error(err.message || 'Action failed'));
}, []);
const handleTrigger = useCallback((agentId) => {
api.triggerFeatureAgent(agentId).then(() => toast.success('Run triggered')).catch(() => {});
api.triggerFeatureAgent(agentId).then(() => toast.success('Run triggered')).catch(err => toast.error(err.message || 'Action failed'));
}, []);

const handleSave = useCallback((saved) => {
Expand Down
12 changes: 6 additions & 6 deletions client/src/pages/FeatureAgents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,41 +33,41 @@ export default function FeatureAgents() {
api.startFeatureAgent(id).then(agent => {
setAgents(prev => prev.map(a => a.id === id ? { ...a, ...agent } : a));
toast.success('Feature agent activated');
}).catch(() => {});
}).catch(err => toast.error(err.message || 'Action failed'));
}, []);

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

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

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

const handleTrigger = useCallback((id) => {
api.triggerFeatureAgent(id).then(() => {
toast.success('Run triggered');
}).catch(() => {});
}).catch(err => toast.error(err.message || 'Action failed'));
}, []);

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

const activeCount = agents.filter(a => a.status === 'active').length;
Expand Down
16 changes: 10 additions & 6 deletions client/src/pages/Jira.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function Jira() {
const response = await api.get('/jira/instances');
setInstances(response.instances || {});
} catch (error) {
console.error('Failed to load JIRA instances:', error);
console.error(`Failed to load JIRA instances: ${error.message}`);
toast.error(`Failed to load JIRA instances: ${error.message}`);
} finally {
setLoading(false);
Expand Down Expand Up @@ -102,13 +102,13 @@ export default function Jira() {
apiToken: formData.apiToken
};

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

toast.success(`JIRA instance "${payload.name}" saved successfully`);
await loadInstances();
setInstances(prev => ({ ...prev, [saved.id]: saved }));
handleCancel();
} catch (error) {
console.error('Failed to save JIRA instance:', error);
console.error(`Failed to save JIRA instance: ${error.message}`);
setSaveError(error.message);
toast.error(`Failed to save: ${error.message}`);
} finally {
Expand All @@ -127,9 +127,13 @@ export default function Jira() {
try {
await api.delete(`/jira/instances/${instanceId}`);
toast.success(`JIRA instance "${instanceId}" deleted`);
await loadInstances();
setInstances(prev => {
const next = { ...prev };
delete next[instanceId];
return next;
});
} catch (error) {
console.error('Failed to delete JIRA instance:', error);
console.error(`Failed to delete JIRA instance: ${error.message}`);
toast.error(`Failed to delete: ${error.message}`);
}
};
Expand Down
6 changes: 4 additions & 2 deletions server/lib/telegramClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { EventEmitter } from 'events';
const BASE_URL = 'https://api.telegram.org/bot';
const POLL_TIMEOUT_SEC = 30;
const API_TIMEOUT_MS = 10_000; // regular calls: sendMessage, editMessageText, etc.
const RETRY_DELAY_API_ERROR_MS = 5_000;
const RETRY_DELAY_NETWORK_ERROR_MS = 2_000;

function buildUrl(token, method) {
return `${BASE_URL}${token}/${method}`;
Expand Down Expand Up @@ -57,14 +59,14 @@ export function createTelegramBot(token, opts = {}) {
const json = await res.json();
if (!json.ok) {
// Retry after a delay on API errors
await new Promise(r => setTimeout(r, 5000));
await new Promise(r => setTimeout(r, RETRY_DELAY_API_ERROR_MS));
continue;
}
updates = json.result;
} catch {
// Aborted (stopPolling called) or network error
if (!polling) break;
await new Promise(r => setTimeout(r, 2000));
await new Promise(r => setTimeout(r, RETRY_DELAY_NETWORK_ERROR_MS));
continue;
}

Expand Down
Loading