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:
2026-08-17 10:41:24 -05:00
co-authored by Claude Opus 5
parent 5ba33899c2
commit c77fdad2b9
15 changed files with 332 additions and 47 deletions
+17 -1
View File
@@ -1,4 +1,4 @@
import express from 'express';
import express, { Request, Response, NextFunction } from 'express';
import cookieParser from 'cookie-parser';
import path from 'path';
import itemsRouter from './routes/items';
@@ -58,4 +58,20 @@ if (process.env.NODE_ENV !== 'test') {
});
}
// Mounted last, so it sees errors from every route above. Without it, a route
// that hands an error to next() falls through to Express's default handler,
// and — worse — an async route that rejects never responds at all, leaving the
// client hanging. A hung request is indistinguishable from an empty catalogue
// in the UI, which is exactly how a schema mismatch once read as "the store
// has no items". Always answer.
//
// The four-argument signature is what marks this as error middleware; `next`
// is unused but must stay for Express to recognise it.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
if (res.headersSent) return;
res.status(500).json({ error: 'internal error' });
});
export default app;