This commit is contained in:
Your Name committed 2026-06-28 21:34:58 -04:00
1 parent 3965f4f4da
commit 70877882b0
82 files changed
+4353 -186

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
# BRIEFING — 2026-06-29T01:20:34Z
## Mission
Analyze codebase and formulate implementation strategy for updating Menu/MenuItem schema, shared types, backend routes, and MCP tools with new image fields.
## 🔒 My Identity
- Archetype: Explorer
- Roles: Read-only investigator
- Working directory: /home/ti/git/homaroma/.agents/explorer_m2_3
- Original parent: 8e727cf6-d863-41ec-9bd2-f8dead86db2a
- Milestone: Milestone 2 (Backend & DB)
## 🔒 Key Constraints
- Read-only investigation — do NOT implement
- Operational code-only mode: NO external web searches, curl, wget, etc.
- No editing files outside of own directory.
## Current Parent
- Conversation ID: 8e727cf6-d863-41ec-9bd2-f8dead86db2a
- Updated: 2026-06-29T01:21:36Z
## Investigation State
- **Explored paths**: `apps/backend/prisma/schema.prisma`, `packages/types/index.ts`, `apps/backend/src/routes/menus.ts`, `apps/mcp-server/menu-tools.ts`, `apps/mcp-server/menu-toolkit.ts`
- **Key findings**:
- `Menu` and `MenuItem` models mapped in Prisma database, requiring `rootImage` (Menu) and `columnImage`/`hoverImage` (MenuItem) nullable columns.
- Shared TypeScript interfaces in `packages/types/index.ts` must expose the same fields.
- Elysia API routes in `menus.ts` require TypeBox validation updates and database payload mapping for POST/PUT.
- MCP Server tools require validation changes in `menu-tools.ts` and tool schema definitions in `menu-toolkit.ts`.
- **Unexplored areas**: None, task is complete.
## Key Decisions Made
- Chose `t.Optional(t.Nullable(t.String()))` for Elysia API payload validation to preserve backward compatibility.
- Determined that database migrations must be run via `bunx prisma migrate dev` locally and `bunx prisma migrate deploy` in production.
- Kept the seed script (`seed.cjs`) unchanged as the new fields are optional.
## Artifact Index
- /home/ti/git/homaroma/.agents/explorer_m2_3/analysis.md — Main analysis and implementation strategy report
- /home/ti/git/homaroma/.agents/explorer_m2_3/handoff.md — Final handoff report
- /home/ti/git/homaroma/.agents/explorer_m2_3/progress.md — Liveness progress heartbeat
+475
View File
@@ -0,0 +1,475 @@
# Implementation Strategy for Menu Image Fields
This report outlines the technical analysis and implementation strategy for adding new image fields to the `Menu` and `MenuItem` models in the backend database, shared types, backend API routes, and MCP tools.
---
## Summary of Findings
- **Database Model**: In `apps/backend/prisma/schema.prisma`, `Menu` holds a flat list of `MenuItem` objects via a relation, and a hierarchical JSON `payload`. To support the layout requirements, `Menu` requires `rootImage` (optional `String?`), while `MenuItem` requires `columnImage` and `hoverImage` (both optional `String?`).
- **TypeScript Types**: `packages/types/index.ts` exposes interface types `Menu` and `MenuItem` directly from source files, so adding the optional image properties there is straightforward.
- **Backend Routes**: `apps/backend/src/routes/menus.ts` defines Elysia routes. The `POST /` and `PUT /:name` endpoints require updating their TypeBox validation schemas (`t.Optional(t.Nullable(t.String()))`) and mapping the new fields in Prisma transactions. `GET` endpoints automatically return these new database fields.
- **MCP Server Tools**: `apps/mcp-server/menu-tools.ts` validates `MenuItem` lists via `validateMenuItems` and requires checks for the new image fields. `apps/mcp-server/menu-toolkit.ts` exposes `upsert_menu` and requires updating its input schema properties and input payload mapping.
---
## 1. Database Schema & Migration Strategy
### File: `apps/backend/prisma/schema.prisma`
#### Existing Code (Lines 638–658)
```prisma
model Menu {
id Int @id @default(autoincrement())
name String @unique // e.g., "header", "footer"
payload Json?
isActive Boolean @default(true)
items MenuItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MenuItem {
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
translations MenuItemTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
#### Proposed Changes
Add `rootImage` to the `Menu` model, and `columnImage`/`hoverImage` to the `MenuItem` model:
```prisma
model Menu {
id Int @id @default(autoincrement())
name String @unique // e.g., "header", "footer"
payload Json?
isActive Boolean @default(true)
rootImage String?
items MenuItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MenuItem {
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
columnImage String?
hoverImage String?
translations MenuItemTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
### Migration Commands
1. **Development Environment**: To generate and apply the SQL migration locally, run:
```bash
bunx prisma migrate dev --name add_menu_image_fields
```
*Warning*: Never use `prisma db push` in this project as it bypasses migrations and will lead to local/production schema drift.
2. **Production/CI Environment**: To apply the created migration to the production database during deployment, run:
```bash
bunx prisma migrate deploy
```
---
## 2. Shared Type Declarations
### File: `packages/types/index.ts`
#### Existing Code (Lines 195–208)
```typescript
export interface MenuItem {
id: number
label: string
url: string
order: number
translations?: any[]
}
export interface Menu {
id: number
name: string
items: MenuItem[]
payload?: any
}
```
#### Proposed Changes
Update interfaces to expose the optional properties:
```typescript
export interface MenuItem {
id: number
label: string
url: string
order: number
columnImage?: string | null
hoverImage?: string | null
translations?: any[]
}
export interface Menu {
id: number
name: string
rootImage?: string | null
items: MenuItem[]
payload?: any
}
```
---
## 3. Backend Route Changes
### File: `apps/backend/src/routes/menus.ts`
Both `POST /` and `PUT /:name` routes must be modified.
#### A. Route `POST /` (Lines 32–59)
##### Before
```typescript
.post("/", async ({ body, set }) => {
try {
const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
const menu = await prisma.menu.create({
data: {
name,
items: items ? {
create: items
} : undefined
},
include: { items: true }
});
return menu;
} catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create menu" };
}
}, {
body: t.Object({
name: t.String(),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
order: t.Number()
})))
})
})
```
##### After (Proposed)
```typescript
.post("/", async ({ body, set }) => {
try {
const { name, rootImage, items } = body as {
name: string;
rootImage?: string | null;
items?: { label: string; url: string; order: number; columnImage?: string | null; hoverImage?: string | null }[]
};
const menu = await prisma.menu.create({
data: {
name,
rootImage,
items: items ? {
create: items
} : undefined
},
include: { items: true }
});
return menu;
} catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create menu" };
}
}, {
body: t.Object({
name: t.String(),
rootImage: t.Optional(t.Nullable(t.String())),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
order: t.Number(),
columnImage: t.Optional(t.Nullable(t.String())),
hoverImage: t.Optional(t.Nullable(t.String()))
})))
})
})
```
#### B. Route `PUT /:name` (Lines 60–147)
##### Before
```typescript
.put("/:name", async ({ params: { name }, body, set }) => {
try {
const { items, payload, isActive } = body as {
payload?: any;
isActive?: boolean;
items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[]
};
let menu = await prisma.menu.findUnique({ where: { name } });
const updateData: any = {};
if (payload !== undefined) updateData.payload = payload;
if (isActive !== undefined) updateData.isActive = isActive;
if (!menu) {
menu = await prisma.menu.create({ data: { name, ...updateData } });
} else if (Object.keys(updateData).length > 0) {
menu = await prisma.menu.update({ where: { name }, data: updateData });
}
await prisma.$transaction(async (tx: any) => {
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu!.id } });
const existingItemIds = existingItems.map((i: any) => i.id);
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
if (itemsToDelete.length > 0) {
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
}
for (const item of items) {
if (item.id && existingItemIds.includes(item.id)) {
await tx.menuItem.update({
where: { id: item.id },
data: { label: item.label, url: item.url, order: item.order }
});
if (item.translations?.length) {
for (const t of item.translations) {
await tx.menuItemTranslation.upsert({
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
update: { label: t.label },
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
});
}
}
} else {
await tx.menuItem.create({
data: {
menuId: menu!.id,
label: item.label,
url: item.url,
order: item.order,
translations: item.translations?.length ? {
create: item.translations.map(t => ({
langCode: t.langCode,
label: t.label
}))
} : undefined
}
});
}
}
});
// ... (retrieval and return logic)
}, {
body: t.Object({
payload: t.Optional(t.Any()),
isActive: t.Optional(t.Boolean()),
items: t.Array(t.Object({
id: t.Optional(t.Number()),
label: t.String(),
url: t.String(),
order: t.Number(),
translations: t.Optional(t.Array(t.Object({
langCode: t.String(),
label: t.String()
})))
}))
})
})
```
##### After (Proposed)
```typescript
.put("/:name", async ({ params: { name }, body, set }) => {
try {
const { items, payload, isActive, rootImage } = body as {
payload?: any;
isActive?: boolean;
rootImage?: string | null;
items: {
id?: number;
label: string;
url: string;
order: number;
columnImage?: string | null;
hoverImage?: string | null;
translations?: { langCode: string; label: string }[]
}[]
};
let menu = await prisma.menu.findUnique({ where: { name } });
const updateData: any = {};
if (payload !== undefined) updateData.payload = payload;
if (isActive !== undefined) updateData.isActive = isActive;
if (rootImage !== undefined) updateData.rootImage = rootImage;
if (!menu) {
menu = await prisma.menu.create({ data: { name, ...updateData } });
} else if (Object.keys(updateData).length > 0) {
menu = await prisma.menu.update({ where: { name }, data: updateData });
}
await prisma.$transaction(async (tx: any) => {
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu!.id } });
const existingItemIds = existingItems.map((i: any) => i.id);
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
if (itemsToDelete.length > 0) {
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
}
for (const item of items) {
if (item.id && existingItemIds.includes(item.id)) {
await tx.menuItem.update({
where: { id: item.id },
data: {
label: item.label,
url: item.url,
order: item.order,
columnImage: item.columnImage,
hoverImage: item.hoverImage
}
});
if (item.translations?.length) {
for (const t of item.translations) {
await tx.menuItemTranslation.upsert({
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
update: { label: t.label },
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
});
}
}
} else {
await tx.menuItem.create({
data: {
menuId: menu!.id,
label: item.label,
url: item.url,
order: item.order,
columnImage: item.columnImage,
hoverImage: item.hoverImage,
translations: item.translations?.length ? {
create: item.translations.map(t => ({
langCode: t.langCode,
label: t.label
}))
} : undefined
}
});
}
}
});
// ... (retrieval and return logic)
}, {
body: t.Object({
payload: t.Optional(t.Any()),
isActive: t.Optional(t.Boolean()),
rootImage: t.Optional(t.Nullable(t.String())),
items: t.Array(t.Object({
id: t.Optional(t.Number()),
label: t.String(),
url: t.String(),
order: t.Number(),
columnImage: t.Optional(t.Nullable(t.String())),
hoverImage: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Array(t.Object({
langCode: t.String(),
label: t.String()
})))
}))
})
})
```
---
## 4. MCP Server Tools Adaptations
### A. File: `apps/mcp-server/menu-tools.ts`
The `validateMenuItems` helper function needs to allow validating `columnImage` and `hoverImage` when present inside items.
#### Proposed Change
Add validation logic inside the `items.forEach` block:
```typescript
if (item.columnImage !== undefined && item.columnImage !== null && typeof item.columnImage !== "string") {
issues.push(`items[${index}].columnImage must be a string when provided`);
}
if (item.hoverImage !== undefined && item.hoverImage !== null && typeof item.hoverImage !== "string") {
issues.push(`items[${index}].hoverImage must be a string when provided`);
}
```
### B. File: `apps/mcp-server/menu-toolkit.ts`
The `upsert_menu` MCP tool schema and handler must be updated to accept and forward the new fields.
#### Proposed Changes
1. **Schema Update (`MENU_TOOL_DEFINITIONS`)**:
Under `upsert_menu.inputSchema.properties`, add `rootImage`:
```typescript
rootImage: { type: "string", description: "Optional image URL for the menu root (optional, nullable)" },
```
Under `upsert_menu.inputSchema.properties.items.items.properties`, add `columnImage` and `hoverImage`:
```typescript
columnImage: { type: "string", description: "Optional column image URL for this menu item" },
hoverImage: { type: "string", description: "Optional hover image URL for this menu item" },
```
2. **Handler Update (`handleMenuTool`)**:
In `case "upsert_menu":`, map `rootImage` into the PUT request payload `body`:
```typescript
if (params.rootImage !== undefined) {
body.rootImage = params.rootImage;
} else if (existingMenu?.rootImage !== undefined) {
body.rootImage = existingMenu.rootImage;
}
```
Also update the dryRun response preview block to display the rootImage state:
```typescript
preview: {
name: params.name,
itemCount: items.length,
hasPayload: !!body.payload,
isActive: body.isActive,
rootImage: body.rootImage // Added preview
}
```
---
## 5. Potential Risks & Mitigation Plans
1. **Local and Production Environment Schema Mismatch**:
- **Risk**: Directly deploying backend code without applying schema changes can lead to runtime crashes due to undefined columns.
- **Mitigation**: Schema migrations (`prisma migrate dev`) must be run and verified in local/staging before merging changes to main. In production, migrations must run (`prisma migrate deploy`) prior to starting the new server container instances.
2. **Nullable Fields on Client Side**:
- **Risk**: Since the new image fields are optional (`String?`), they default to `null` for all existing menus. Client applications (SPA, website storefront, admin dashboard) must render layouts gracefully when these fields are missing.
- **Mitigation**: Code on client sides must fallback to default placeholders (like mock assets or empty spaces) when the image URLs are null or empty.
3. **Elysia TypeScript Transpilation Sync**:
- **Risk**: The backend routes are transpiled into `.js` and `.d.ts` inside the same source directory (`apps/backend/src/routes/`). Discrepancies between `.ts` source edits and build outputs could cause runtime type mismatch or route resolution bugs.
- **Mitigation**: A clean backend rebuild (e.g. compiling with TSC/Bun compiler) should be executed right after changes are made to ensure source code and build assets are in perfect alignment.
4. **Seed Execution Safety**:
- **Risk**: Database seeding in `seed.cjs` upserts menus and menu items. If database constraints are strict, omitting the new columns might fail.
- **Mitigation**: In Prisma, optional columns without default values automatically default to `null` and are not required during `create`/`upsert` calls. Thus, `seed.cjs` will not break, but documenting this compatibility is critical.
+171
View File
@@ -0,0 +1,171 @@
# Handoff Report — Explorer 3, Milestone 2
## 1. Observation
Direct observations from codebase inspection:
1. **Prisma Models**:
File: `apps/backend/prisma/schema.prisma`, lines 638–658:
```prisma
model Menu {
id Int @id @default(autoincrement())
name String @unique // e.g., "header", "footer"
payload Json?
isActive Boolean @default(true)
items MenuItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MenuItem {
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
translations MenuItemTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
2. **TypeScript Interfaces**:
File: `packages/types/index.ts`, lines 195–208:
```typescript
export interface MenuItem {
id: number
label: string
url: string
order: number
translations?: any[]
}
export interface Menu {
id: number
name: string
items: MenuItem[]
payload?: any
}
```
3. **Elysia Backend Routes**:
File: `apps/backend/src/routes/menus.ts`, lines 34–58:
```typescript
const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
...
body: t.Object({
name: t.String(),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
order: t.Number()
})))
})
```
And lines 62–66:
```typescript
const { items, payload, isActive } = body as {
payload?: any;
isActive?: boolean;
items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[]
};
```
4. **MCP Validation Helper**:
File: `apps/mcp-server/menu-tools.ts`, lines 158–170:
```typescript
function validateMenuItems(items: any) {
const issues: string[] = [];
const warnings: string[] = [];
if (!Array.isArray(items)) {
issues.push("items must be an array");
return { issues, warnings };
}
items.forEach((item, index) => {
if (!isPlainObject(item)) {
issues.push(`items[${index}] must be a plain object`);
return;
}
```
5. **MCP Schema**:
File: `apps/mcp-server/menu-toolkit.ts`, lines 34–74, the definition of `upsert_menu`:
```typescript
{
name: "upsert_menu",
description: "Update or create a complete menu by replacing its items. Optional payload/isActive support covers tree-based menus and menu state. Uses proper diff & sync for translations.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name of the menu (e.g. main, footer-explore)" },
dryRun: { type: "boolean", description: "Preview the mutation without saving it" },
payload: {
oneOf: [{ type: "object" }, { type: "array" }],
description: "Optional menu payload for tree-based menus such as main"
},
isActive: { type: "boolean", description: "Optional active state for the menu" },
items: {
type: "array",
description: "Array of menu items",
items: {
type: "object",
properties: {
id: { type: "number", description: "ID of existing item (optional)" },
label: { type: "string", description: "Label of the menu item" },
url: { type: "string", description: "URL of the menu item" },
order: { type: "number", description: "Order sequence" },
translations: {
```
---
## 2. Logic Chain
1. **Database Schema**: To store `rootImage` for Menu, and `columnImage`/`hoverImage` for MenuItem, we must add nullable string fields (`String?`) to these models in `schema.prisma`.
2. **TypeScript Declarations**: The TypeScript project imports standard types from `@homaroma/types` (`packages/types/index.ts`). These interfaces must match the database schemas by adding `rootImage?: string | null`, `columnImage?: string | null`, and `hoverImage?: string | null`.
3. **API Validation**: The backend validates POST/PUT requests using Elysia TypeBox (`t.Object`). Adding the new fields as optional nullable strings (`t.Optional(t.Nullable(t.String()))`) enables the backend to validate incoming values correctly while maintaining backward compatibility.
4. **API Mapping**: The database update transactions inside `menus.ts` map the validated payload to Prisma database queries. We must update the `create` and `update` queries to assign `rootImage` for Menu, and `columnImage`/`hoverImage` for MenuItem.
5. **MCP Schema & Helper**: The MCP toolkit contains utility validation for items before sending PUT requests. Adding validation rules in `validateMenuItems` prevents MCP validation blocks. Updating `MENU_TOOL_DEFINITIONS` exposes the new input parameters to AI clients interacting with `upsert_menu`.
---
## 3. Caveats
- **Seeding Script**: The seed file (`apps/backend/prisma/seed.cjs`) upserts default core menus. It does not contain image values, but since the new fields are fully optional, no updates are strictly required there.
- **Frontend Fallbacks**: Client code (SPA and Website) must handle null values safely. The actual layout rendering logic (DesktopMegaMenu) is a separate frontend concern that must be updated dynamically to read these fields.
- **Transpiled Route Artifacts**: There are transpiled `.js` and `.d.ts` files inside the routes folder, meaning a clean build step is required after modifications to typescript source files.
---
## 4. Conclusion
The strategy outlines a safe, fully backward-compatible database, route, and MCP tool update. The exact changes required in the four files have been mapped and documented in detail in `analysis.md`. The next step is to delegate the implementation tasks to the Implementer agent.
---
## 5. Verification Method
To independently verify the final implementation:
1. **Schema Check**:
Confirm that the prisma client generates successfully by running:
```bash
bunx prisma generate
```
2. **Database Migration Status**:
Run the following command to ensure migrations are applied cleanly without conflicts:
```bash
bunx prisma migrate status
```
3. **Route Integration & Tests**:
Execute the backend build script to verify there are no syntax or type mismatch errors:
```bash
npm run build --workspace apps/backend
```
Execute the workspace test command:
```bash
npm run test
```
+12
View File
@@ -0,0 +1,12 @@
## 2026-06-29T01:20:34Z
You are Explorer 3 for Milestone 2: Backend & DB.
Your working directory is /home/ti/git/homaroma/.agents/explorer_m2_3.
Please analyze the codebase and formulate a precise implementation strategy for updating:
1. Prisma schema (`apps/backend/prisma/schema.prisma`): Add rootImage (String?, optional) to Menu, and columnImage (String?, optional) and hoverImage (String?, optional) to MenuItem. Explain what migration commands are needed.
2. Shared types (`packages/types/index.ts`): Expose the new fields.
3. Backend route (`apps/backend/src/routes/menus.ts`): Modify the endpoints (GET, POST, etc.) to handle, validate, save, and return these fields.
4. MCP Server tools (`apps/mcp-server/menu-tools.ts` and `apps/mcp-server/menu-toolkit.ts`): Check if any MCP tools need updating to support these new fields (both inputs and return values).
Read these files, verify their structure, check zod validators or Prisma types, and write your report to /home/ti/git/homaroma/.agents/explorer_m2_3/analysis.md. Summarize your findings, the exact file changes needed (with lines if possible), and any potential risks. Do not implement.
Once done, send a message back with your findings and the path to your analysis.md.
+13
View File
@@ -0,0 +1,13 @@
# Progress — 2026-06-29T01:20:34Z
Last visited: 2026-06-29T01:21:36Z
- [x] Initialized ORIGINAL_REQUEST.md
- [x] Created BRIEFING.md
- [x] Read backend-mcp-lead SKILL.md
- [x] Explore Prisma schema (`apps/backend/prisma/schema.prisma`)
- [x] Explore shared types (`packages/types/index.ts`)
- [x] Explore backend routes (`apps/backend/src/routes/menus.ts`)
- [x] Explore MCP server tools (`apps/mcp-server/menu-tools.ts` and `apps/mcp-server/menu-toolkit.ts`)
- [x] Formulate implementation strategy and write `analysis.md`
- [x] Create `handoff.md` and send completion message to parent