Spaces:
Sleeping
Sleeping
Improve running dashboard error handling
Browse files- server/index.js +13 -1
- src/App.js +21 -8
- src/components/RunForm.js +3 -2
- src/components/RunLog.js +7 -5
- src/utils/storage.js +20 -1
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(
|
| 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 |
-
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
}
|
| 78 |
|
|
@@ -83,8 +92,10 @@ function App() {
|
|
| 83 |
prev.map((r) => (r.id === id ? updated : r))
|
| 84 |
);
|
| 85 |
setSyncError('');
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 97 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|