lewtun HF Staff commited on
Commit
ed11f02
·
1 Parent(s): 48e9fcc

Improve running dashboard error handling

Browse files
server/index.js CHANGED
@@ -15,6 +15,18 @@ const HUB_DATASET_FILE = process.env.HF_DATASET_FILE || 'runs.json';
15
 
16
  app.use(express.json({ limit: '1mb' }));
17
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  function sortRunsByDateDesc(runs) {
19
  return [...runs].sort((a, b) => {
20
  const byDate = (b?.date || '').localeCompare(a?.date || '');
@@ -208,7 +220,7 @@ if (process.env.NODE_ENV === 'production') {
208
  app.use((error, _req, res, _next) => {
209
  // eslint-disable-next-line no-console
210
  console.error(error);
211
- res.status(500).json({ error: 'Internal server error' });
212
  });
213
 
214
  app.listen(PORT, () => {
 
15
 
16
  app.use(express.json({ limit: '1mb' }));
17
 
18
+ function getErrorStatus(error) {
19
+ const status = error?.statusCode || error?.status;
20
+ return Number.isInteger(status) && status >= 400 && status < 600 ? status : 500;
21
+ }
22
+
23
+ function getErrorMessage(error) {
24
+ if (error && typeof error.message === 'string' && error.message.trim()) {
25
+ return error.message.trim();
26
+ }
27
+ return 'Internal server error';
28
+ }
29
+
30
  function sortRunsByDateDesc(runs) {
31
  return [...runs].sort((a, b) => {
32
  const byDate = (b?.date || '').localeCompare(a?.date || '');
 
220
  app.use((error, _req, res, _next) => {
221
  // eslint-disable-next-line no-console
222
  console.error(error);
223
+ res.status(getErrorStatus(error)).json({ error: getErrorMessage(error) });
224
  });
225
 
226
  app.listen(PORT, () => {
src/App.js CHANGED
@@ -11,6 +11,13 @@ import Charts from './components/Charts';
11
  import RunLog from './components/RunLog';
12
  import './App.css';
13
 
 
 
 
 
 
 
 
14
  function App() {
15
  const [runs, setRuns] = useState([]);
16
  const [isLoading, setIsLoading] = useState(true);
@@ -26,9 +33,9 @@ function App() {
26
  setRuns(Array.isArray(loaded) ? loaded : []);
27
  setSyncError('');
28
  }
29
- } catch {
30
  if (active) {
31
- setSyncError('Could not load shared run data.');
32
  }
33
  } finally {
34
  if (active) setIsLoading(false);
@@ -71,8 +78,10 @@ function App() {
71
  const created = await createRun(runWithId);
72
  setRuns((prev) => [created, ...prev.filter((run) => run.id !== created.id)]);
73
  setSyncError('');
74
- } catch {
75
- setSyncError('Could not save run.');
 
 
76
  }
77
  }
78
 
@@ -83,8 +92,10 @@ function App() {
83
  prev.map((r) => (r.id === id ? updated : r))
84
  );
85
  setSyncError('');
86
- } catch {
87
- setSyncError('Could not update run.');
 
 
88
  }
89
  }
90
 
@@ -93,8 +104,10 @@ function App() {
93
  await deleteRun(id);
94
  setRuns((prev) => prev.filter((r) => r.id !== id));
95
  setSyncError('');
96
- } catch {
97
- setSyncError('Could not delete run.');
 
 
98
  }
99
  }
100
 
 
11
  import RunLog from './components/RunLog';
12
  import './App.css';
13
 
14
+ function getUserMessage(error, fallback) {
15
+ if (error instanceof Error && error.message.trim()) {
16
+ return error.message.trim();
17
+ }
18
+ return fallback;
19
+ }
20
+
21
  function App() {
22
  const [runs, setRuns] = useState([]);
23
  const [isLoading, setIsLoading] = useState(true);
 
33
  setRuns(Array.isArray(loaded) ? loaded : []);
34
  setSyncError('');
35
  }
36
+ } catch (error) {
37
  if (active) {
38
+ setSyncError(`Load failed: ${getUserMessage(error, 'Could not load shared run data.')}`);
39
  }
40
  } finally {
41
  if (active) setIsLoading(false);
 
78
  const created = await createRun(runWithId);
79
  setRuns((prev) => [created, ...prev.filter((run) => run.id !== created.id)]);
80
  setSyncError('');
81
+ return true;
82
+ } catch (error) {
83
+ setSyncError(`Save failed: ${getUserMessage(error, 'Could not save run.')}`);
84
+ return false;
85
  }
86
  }
87
 
 
92
  prev.map((r) => (r.id === id ? updated : r))
93
  );
94
  setSyncError('');
95
+ return true;
96
+ } catch (error) {
97
+ setSyncError(`Update failed: ${getUserMessage(error, 'Could not update run.')}`);
98
+ return false;
99
  }
100
  }
101
 
 
104
  await deleteRun(id);
105
  setRuns((prev) => prev.filter((r) => r.id !== id));
106
  setSyncError('');
107
+ return true;
108
+ } catch (error) {
109
+ setSyncError(`Delete failed: ${getUserMessage(error, 'Could not delete run.')}`);
110
+ return false;
111
  }
112
  }
113
 
src/components/RunForm.js CHANGED
@@ -31,7 +31,7 @@ function RunForm({ onAddRun }) {
31
  Object.fromEntries(INJURY_LOCATIONS.map((loc) => [loc.key, { enabled: false, during: '', after: '' }]))
32
  );
33
 
34
- function handleSubmit(e) {
35
  e.preventDefault();
36
  const dist = parseFloat(distance);
37
  const mins = parseFloat(time);
@@ -54,7 +54,8 @@ function RunForm({ onAddRun }) {
54
  }
55
  }
56
 
57
- onAddRun(runData);
 
58
 
59
  setDistance('');
60
  setTime('');
 
31
  Object.fromEntries(INJURY_LOCATIONS.map((loc) => [loc.key, { enabled: false, during: '', after: '' }]))
32
  );
33
 
34
+ async function handleSubmit(e) {
35
  e.preventDefault();
36
  const dist = parseFloat(distance);
37
  const mins = parseFloat(time);
 
54
  }
55
  }
56
 
57
+ const saved = await onAddRun(runData);
58
+ if (!saved) return;
59
 
60
  setDistance('');
61
  setTime('');
src/components/RunLog.js CHANGED
@@ -76,7 +76,7 @@ function RunLog({ runs, onEditRun, onDeleteRun }) {
76
  }));
77
  }
78
 
79
- function handleSave() {
80
  const dist = parseFloat(editForm.distance_km);
81
  const mins = parseFloat(editForm.time_minutes);
82
  const cadence = editForm.cadence_spm === '' ? null : parseInt(editForm.cadence_spm, 10);
@@ -117,17 +117,19 @@ function RunLog({ runs, onEditRun, onDeleteRun }) {
117
  }
118
  }
119
 
120
- onEditRun(editingId, updated);
121
- setEditingId(null);
 
 
122
  }
123
 
124
  function handleCancel() {
125
  setEditingId(null);
126
  }
127
 
128
- function handleDelete(id) {
129
  if (window.confirm('Delete this run?')) {
130
- onDeleteRun(id);
131
  }
132
  }
133
 
 
76
  }));
77
  }
78
 
79
+ async function handleSave() {
80
  const dist = parseFloat(editForm.distance_km);
81
  const mins = parseFloat(editForm.time_minutes);
82
  const cadence = editForm.cadence_spm === '' ? null : parseInt(editForm.cadence_spm, 10);
 
117
  }
118
  }
119
 
120
+ const saved = await onEditRun(editingId, updated);
121
+ if (saved) {
122
+ setEditingId(null);
123
+ }
124
  }
125
 
126
  function handleCancel() {
127
  setEditingId(null);
128
  }
129
 
130
+ async function handleDelete(id) {
131
  if (window.confirm('Delete this run?')) {
132
+ await onDeleteRun(id);
133
  }
134
  }
135
 
src/utils/storage.js CHANGED
@@ -8,7 +8,26 @@ async function request(path, options = {}) {
8
  });
9
 
10
  if (!response.ok) {
11
- throw new Error(`API ${response.status}: ${response.statusText}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
  if (response.status === 204) return null;
 
8
  });
9
 
10
  if (!response.ok) {
11
+ let message = `API ${response.status}: ${response.statusText}`;
12
+ const contentType = response.headers.get('content-type') || '';
13
+
14
+ try {
15
+ if (contentType.includes('application/json')) {
16
+ const payload = await response.json();
17
+ if (payload && typeof payload.error === 'string' && payload.error.trim()) {
18
+ message = payload.error.trim();
19
+ }
20
+ } else {
21
+ const text = await response.text();
22
+ if (text.trim()) {
23
+ message = text.trim();
24
+ }
25
+ }
26
+ } catch {
27
+ // Fall back to the HTTP status message when the error body is unreadable.
28
+ }
29
+
30
+ throw new Error(message);
31
  }
32
 
33
  if (response.status === 204) return null;