fix: run migrations on boot and stop failures rendering as empty (#23)
The storefront showed no inventory after deploying the categories/tags release. No data was lost: the code queried categories/item_tags/ items.category_id against a database where the migration had not been run, and that failure was invisible at every layer. Three changes, each addressing one layer: Migrations now run at container start, so deployed code cannot be ahead of the schema and the easily-forgotten manual `docker exec migrate.js up` step disappears. migrate.js waits for Postgres to accept connections first, since the NAS brings the DB container up slower than the app, and still exits non-zero so a bad migration stops the container rather than serving a half-migrated schema. Express 4 does not forward a rejected async handler, and no error middleware was mounted, so a failing query never responded at all. Async routes are now wrapped and an error middleware guarantees a 500. A hung request is indistinguishable from an empty result in the UI, which is how a schema mismatch came to read as "the store has no items". The storefront now separates "request failed" from "no items" and offers a retry. fetchItems/fetchFilterOptions throw on a non-OK response rather than returning the parsed error body, which would have been set as the item list and crashed the grid on .map. Also restores the project-context update from 7fb5764, which was left out of PR #24 and ended up dangling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
-4
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Layout, Typography, Switch, Row, Col, Spin, Button, theme, Badge, Empty } from 'antd';
|
||||
import { Layout, Typography, Switch, Row, Col, Spin, Button, theme, Badge, Empty, Alert } from 'antd';
|
||||
import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api';
|
||||
@@ -27,6 +27,7 @@ const FILTER_DEBOUNCE_MS = 250;
|
||||
export default function App() {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [options, setOptions] = useState<FilterOptions | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -55,7 +56,14 @@ export default function App() {
|
||||
|
||||
const load = useCallback(() => {
|
||||
return fetchItems(filtersFromSearchParams(new URLSearchParams(filterKey)))
|
||||
.then(setItems)
|
||||
.then((loaded) => {
|
||||
setItems(loaded);
|
||||
setFailed(false);
|
||||
})
|
||||
// A failed request must never fall through to the empty state: telling a
|
||||
// customer "no items yet" when the server is broken hides the outage and
|
||||
// reads as an empty shop.
|
||||
.catch(() => setFailed(true))
|
||||
.finally(() => setLoading(false));
|
||||
}, [filterKey]);
|
||||
|
||||
@@ -124,8 +132,16 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading && !items.length ? <Spin /> : null}
|
||||
{!loading && !items.length ? (
|
||||
{loading && !items.length && !failed ? <Spin /> : null}
|
||||
{failed ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="Couldn't load items"
|
||||
description="The server didn't return the catalogue. This is usually temporary."
|
||||
action={<Button size="small" onClick={() => { setLoading(true); load(); }}>Retry</Button>}
|
||||
/>
|
||||
) : !loading && !items.length ? (
|
||||
<Empty
|
||||
description={
|
||||
hasActiveFilters(filters)
|
||||
|
||||
Reference in New Issue
Block a user