feat: admin Ingredients CRUD UI; fix "page not found" after deleting shopping list/recipe/collection (v0.85.0)
Admin > Ingredients lets admins add/edit/delete canonical ingredients and their aliases (e.g. "sel"/"sel fin"/"table salt") without touching code — previously only settable by hand in packages/db/src/seed.ts. Deleting just unlinks referencing pantry items/recipe ingredients (onDelete: set null), nothing else changes. Fixed: deleting a shopping list, recipe, or collection from its own detail page used router.push to navigate away, leaving the deleted resource's URL in browser history — one back-button press landed on a real "Page not found" screen. All three now use router.replace so the dead URL never sits in history. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
||||
export const APP_VERSION = "0.84.0";
|
||||
export const APP_VERSION = "0.85.0";
|
||||
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
@@ -11,6 +11,16 @@ export type ChangelogEntry = {
|
||||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "0.85.0",
|
||||
date: "2026-07-24 21:00",
|
||||
added: [
|
||||
"Admin > Ingredients: manage canonical ingredients and their name variants (e.g. \"sel\"/\"sel fin\"/\"table salt\") without a code change — previously only editable by hand in the seed file.",
|
||||
],
|
||||
fixed: [
|
||||
"Deleting a shopping list, recipe, or collection from its own page could leave you one back-button press away from a \"Page not found\" screen — the deleted URL stayed in browser history. Now replaces the history entry instead of pushing a new one.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.84.0",
|
||||
date: "2026-07-24 20:00",
|
||||
|
||||
@@ -975,6 +975,16 @@ export function generateOpenApiSpec(): object {
|
||||
enabled: z.boolean(),
|
||||
}));
|
||||
|
||||
const IngredientRef = registry.register("Ingredient", z.object({
|
||||
id: z.string(), name: z.string(), aliases: z.array(z.string()), category: z.string().nullable(),
|
||||
}));
|
||||
const UpsertIngredientRef = registry.register("UpsertIngredient", z.object({
|
||||
name: z.string().min(1).max(100), aliases: z.array(z.string().min(1).max(100)).max(50).default([]), category: z.string().max(50).optional(),
|
||||
}));
|
||||
registry.registerPath({ method: "get", path: "/api/v1/admin/ingredients", summary: "List canonical ingredients", description: "Admin only. Used for name/alias matching (\"sel\"/\"sel fin\"/\"table salt\" recognized as the same ingredient) across pantry, can-cook scoring, auto-deduct, and shopping-list generation.", security: adminSecurity, responses: { 200: { description: "Ingredients", content: { "application/json": { schema: z.object({ data: z.array(IngredientRef) }) } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "post", path: "/api/v1/admin/ingredients", summary: "Create a canonical ingredient", description: "Admin only. name must be unique.", security: adminSecurity, request: { body: { content: { "application/json": { schema: UpsertIngredientRef } }, required: true } }, responses: { 201: { description: "Created", content: { "application/json": { schema: z.object({ id: z.string() }) } } }, 400: { description: "Validation error", content: { "application/json": { schema: ApiErrorRef } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } }, 409: { description: "Name already exists", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "put", path: "/api/v1/admin/ingredients/{id}", summary: "Update a canonical ingredient", description: "Admin only.", security: adminSecurity, request: { params: idParam, body: { content: { "application/json": { schema: UpsertIngredientRef.partial() } }, required: true } }, responses: { 200: { description: "Updated", content: { "application/json": { schema: z.object({ updated: z.boolean() }) } } }, 400: { description: "Validation error", content: { "application/json": { schema: ApiErrorRef } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } }, 409: { description: "Name already exists", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "delete", path: "/api/v1/admin/ingredients/{id}", summary: "Delete a canonical ingredient", description: "Admin only. Unlinks (doesn't cascade-delete) any pantry item or recipe ingredient that referenced it.", security: adminSecurity, request: { params: idParam }, responses: { 204: { description: "Deleted" }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "get", path: "/api/v1/admin/feature-flags", summary: "Get the full feature x tier toggle matrix", description: "Admin only.", security: adminSecurity, responses: { 200: { description: "Matrix", content: { "application/json": { schema: FeatureFlagMatrixRef } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "patch", path: "/api/v1/admin/feature-flags", summary: "Enable or disable a feature for a tier", description: "Admin only. Disabling a feature doesn't hide its button client-side — the corresponding AI route (variations/drinks/pairings) returns 403 with code FEATURE_DISABLED for users on that tier, and the UI shows an upgrade prompt instead.", security: adminSecurity, request: { body: { content: { "application/json": { schema: UpdateFeatureFlagRef } }, required: true } }, responses: { 200: { description: "Saved", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Validation error", content: { "application/json": { schema: ApiErrorRef } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user