feat(F-071): completed feature
This commit is contained in:
@@ -3446,6 +3446,41 @@
|
|||||||
"close": true
|
"close": true
|
||||||
},
|
},
|
||||||
"completed_at": "2026-08-19T16:21:01Z"
|
"completed_at": "2026-08-19T16:21:01Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F-071",
|
||||||
|
"type": "feature",
|
||||||
|
"title": "Editable emoji and color for categories",
|
||||||
|
"problem": "Categories use hardcoded emoji/color maps that cannot be customized in admin",
|
||||||
|
"goal": "Add emoji and color fields to categories, make them editable in admin form, and use stored values in frontend category displays instead of hardcoded maps",
|
||||||
|
"scope_in": [
|
||||||
|
"backend:category domain",
|
||||||
|
"frontend home grid",
|
||||||
|
"frontend categories list",
|
||||||
|
"admin category form"
|
||||||
|
],
|
||||||
|
"scope_out": [
|
||||||
|
"admin redesign",
|
||||||
|
"brand/product changes"
|
||||||
|
],
|
||||||
|
"priority": "high",
|
||||||
|
"risk": "low",
|
||||||
|
"description": "Problem: Categories use hardcoded emoji/color maps that cannot be customized in admin. Goal: Add emoji and color fields to categories, make them editable in admin form, and use stored values in frontend category displays instead of hardcoded maps. Scope IN: backend:category domain, frontend home grid, frontend categories list, admin category form. Scope OUT: admin redesign, brand/product changes. Type: feature. Priority: high. Risk: low.",
|
||||||
|
"acceptance": [
|
||||||
|
"Admin can edit emoji and color per category in the category form",
|
||||||
|
"Frontend category cards use stored emoji and color instead of hardcoded slug-based maps",
|
||||||
|
"New parent categories show their stored emoji instead of generic folder icon",
|
||||||
|
"verify.sh is green"
|
||||||
|
],
|
||||||
|
"status": "done",
|
||||||
|
"created_at": "2026-08-19",
|
||||||
|
"gates": {
|
||||||
|
"reviewer": true,
|
||||||
|
"security": true,
|
||||||
|
"qa": true,
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-19T17:04:41Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function CategoryRow({ cat, onEdit, onDelete }: { cat: Category; onEdit: (c: Cat
|
|||||||
<tr className="hover:bg-gray-50 transition-colors">
|
<tr className="hover:bg-gray-50 transition-colors">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{cat.children && cat.children.length > 0 && <span className="text-gray-300">📁</span>}
|
{cat.children && cat.children.length > 0 && <span className="text-gray-300">{cat.emoji ?? '📁'}</span>}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">{cat.name}</p>
|
<p className="text-sm font-medium text-gray-900">{cat.name}</p>
|
||||||
<p className="text-xs text-gray-400">/{cat.slug}</p>
|
<p className="text-xs text-gray-400">/{cat.slug}</p>
|
||||||
@@ -59,6 +59,8 @@ interface FormState {
|
|||||||
description: string;
|
description: string;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
isParent: boolean;
|
isParent: boolean;
|
||||||
|
emoji: string;
|
||||||
|
color: string;
|
||||||
seoTitle: string;
|
seoTitle: string;
|
||||||
seoTitleManual: boolean;
|
seoTitleManual: boolean;
|
||||||
seoDescription: string;
|
seoDescription: string;
|
||||||
@@ -72,6 +74,8 @@ const EMPTY_FORM: FormState = {
|
|||||||
description: '',
|
description: '',
|
||||||
parentId: null,
|
parentId: null,
|
||||||
isParent: false,
|
isParent: false,
|
||||||
|
emoji: '',
|
||||||
|
color: '',
|
||||||
seoTitle: '',
|
seoTitle: '',
|
||||||
seoTitleManual: false,
|
seoTitleManual: false,
|
||||||
seoDescription: '',
|
seoDescription: '',
|
||||||
@@ -140,6 +144,8 @@ export default function CategoriesPage() {
|
|||||||
description: cat.description ?? '',
|
description: cat.description ?? '',
|
||||||
parentId: cat.parentId,
|
parentId: cat.parentId,
|
||||||
isParent: cat.isParent ?? false,
|
isParent: cat.isParent ?? false,
|
||||||
|
emoji: cat.emoji ?? '',
|
||||||
|
color: cat.color ?? '',
|
||||||
seoTitle: (cat as any).seoTitle ?? '',
|
seoTitle: (cat as any).seoTitle ?? '',
|
||||||
seoTitleManual: true,
|
seoTitleManual: true,
|
||||||
seoDescription: (cat as any).seoDescription ?? '',
|
seoDescription: (cat as any).seoDescription ?? '',
|
||||||
@@ -167,6 +173,8 @@ export default function CategoriesPage() {
|
|||||||
description: form.description || undefined,
|
description: form.description || undefined,
|
||||||
parentId: form.parentId,
|
parentId: form.parentId,
|
||||||
isParent: form.isParent,
|
isParent: form.isParent,
|
||||||
|
emoji: form.emoji || undefined,
|
||||||
|
color: form.color || undefined,
|
||||||
seoTitle: form.seoTitle || undefined,
|
seoTitle: form.seoTitle || undefined,
|
||||||
seoDescription: form.seoDescription || undefined,
|
seoDescription: form.seoDescription || undefined,
|
||||||
};
|
};
|
||||||
@@ -286,6 +294,35 @@ export default function CategoriesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Emoji y color de la tarjeta */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Emoji</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
value={form.emoji}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, emoji: e.target.value }))}
|
||||||
|
maxLength={10}
|
||||||
|
placeholder="🥜"
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm text-2xl text-center focus:ring-2 focus:ring-[#2D6A4F] outline-none"
|
||||||
|
/>
|
||||||
|
{form.emoji && <span className="text-3xl">{form.emoji}</span>}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Emoji identificativo que aparecerá en las tarjetas de categoría.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Color de tarjeta</label>
|
||||||
|
<input
|
||||||
|
value={form.color}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, color: e.target.value }))}
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="bg-[#70ad47]/10 text-[#70ad47] ó from-[#70ad47] to-[#40916C]"
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Clase Tailwind para el color de la tarjeta (fondo o gradiente).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* SEO Title */}
|
{/* SEO Title */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ export interface Category {
|
|||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
isParent?: boolean;
|
isParent?: boolean;
|
||||||
|
emoji?: string;
|
||||||
|
color?: string;
|
||||||
children?: Category[];
|
children?: Category[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -36,10 +36,13 @@ export default async function CategoriesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 justify-center">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 justify-center">
|
||||||
{tree.map((cat, i) => (
|
{tree.map((cat, i) => {
|
||||||
<Link key={cat.id} href={`/categories/${cat.slug}`} className="group block">
|
const emoji = cat.emoji ?? icons[cat.slug] ?? '📦';
|
||||||
<div className={`relative overflow-hidden rounded-2xl bg-gradient-to-br ${colors[i % colors.length]} p-6 text-white min-h-[140px] flex flex-col justify-between`}>
|
const colorClass = cat.color ?? colors[i % colors.length];
|
||||||
<div className="absolute top-4 right-4 text-5xl opacity-20">{icons[cat.slug] ?? '📦'}</div>
|
return (
|
||||||
|
<Link key={cat.id} href={`/categories/${cat.slug}`} className="group block">
|
||||||
|
<div className={`relative overflow-hidden rounded-2xl bg-gradient-to-br ${colorClass} p-6 text-white min-h-[140px] flex flex-col justify-between`}>
|
||||||
|
<div className="absolute top-4 right-4 text-5xl opacity-20">{emoji}</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold group-hover:underline">{cat.name}</h2>
|
<h2 className="text-xl font-bold group-hover:underline">{cat.name}</h2>
|
||||||
{cat.seoDescription && (
|
{cat.seoDescription && (
|
||||||
@@ -60,7 +63,8 @@ export default async function CategoriesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ export default async function CategoriesGrid() {
|
|||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
{categories.map((cat, i) => {
|
{categories.map((cat, i) => {
|
||||||
const totalProducts = cat.children ? cat.children.length * 5 : 5;
|
const totalProducts = cat.children ? cat.children.length * 5 : 5;
|
||||||
|
const emoji = cat.emoji ?? icons[cat.slug] ?? '📦';
|
||||||
|
const colorClass = cat.color ?? colors[i % colors.length];
|
||||||
return (
|
return (
|
||||||
<Link key={cat.id} href={`/categories/${cat.slug}`}>
|
<Link key={cat.id} href={`/categories/${cat.slug}`}>
|
||||||
<div className={`p-5 rounded-xl border border-gray-200 hover:border-[#70ad47] transition-all hover:shadow-md bg-white text-center ${colors[i % colors.length]}`}>
|
<div className={`p-5 rounded-xl border border-gray-200 hover:border-[#70ad47] transition-all hover:shadow-md bg-white text-center ${colorClass}`}>
|
||||||
<div className="text-4xl mb-2">{icons[cat.slug] ?? '📦'}</div>
|
<div className="text-4xl mb-2">{emoji}</div>
|
||||||
<h3 className="font-semibold text-gray-900 text-sm">{cat.name}</h3>
|
<h3 className="font-semibold text-gray-900 text-sm">{cat.name}</h3>
|
||||||
<p className="text-xs text-gray-500 mt-1">{totalProducts} productos</p>
|
<p className="text-xs text-gray-500 mt-1">{totalProducts} productos</p>
|
||||||
{cat.children && cat.children.length > 0 && (
|
{cat.children && cat.children.length > 0 && (
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export interface Category {
|
|||||||
seoDescription?: string;
|
seoDescription?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
isParent?: boolean;
|
||||||
|
emoji?: string;
|
||||||
|
color?: string;
|
||||||
children?: Category[];
|
children?: Category[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
project/migrations/030_category_emoji_color.js
Normal file
21
project/migrations/030_category_emoji_color.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
/**
|
||||||
|
* F-071: Adds emoji and color columns to categories_categories.
|
||||||
|
* Allows admin to set a representative emoji and card color per category.
|
||||||
|
* Both columns are nullable — existing categories remain unaffected.
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE categories_categories
|
||||||
|
ADD COLUMN IF NOT EXISTS emoji VARCHAR(10) NULL DEFAULT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS color TEXT NULL DEFAULT NULL
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE categories_categories
|
||||||
|
DROP COLUMN IF EXISTS emoji,
|
||||||
|
DROP COLUMN IF EXISTS color
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -42,6 +42,8 @@ const newCategorySchema = z.object({
|
|||||||
seoTitle: z.string().min(1).max(200).optional().nullable(),
|
seoTitle: z.string().min(1).max(200).optional().nullable(),
|
||||||
seoDescription: z.string().min(1).max(500).optional().nullable(),
|
seoDescription: z.string().min(1).max(500).optional().nullable(),
|
||||||
isParent: z.boolean().optional(),
|
isParent: z.boolean().optional(),
|
||||||
|
emoji: z.string().max(10).optional().nullable(),
|
||||||
|
color: z.string().max(200).optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const categoryPatchSchema = newCategorySchema
|
const categoryPatchSchema = newCategorySchema
|
||||||
@@ -182,6 +184,8 @@ function serializeCategory(category: Category) {
|
|||||||
seoTitle: category.seoTitle,
|
seoTitle: category.seoTitle,
|
||||||
seoDescription: category.seoDescription,
|
seoDescription: category.seoDescription,
|
||||||
isParent: category.isParent,
|
isParent: category.isParent,
|
||||||
|
emoji: category.emoji,
|
||||||
|
color: category.color,
|
||||||
createdAt: category.createdAt.toISOString(),
|
createdAt: category.createdAt.toISOString(),
|
||||||
updatedAt: category.updatedAt.toISOString(),
|
updatedAt: category.updatedAt.toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface Category {
|
|||||||
seoDescription: string | null;
|
seoDescription: string | null;
|
||||||
/** FIX-19: true = contenedor (puede tener hijos); false = hoja (child). */
|
/** FIX-19: true = contenedor (puede tener hijos); false = hoja (child). */
|
||||||
isParent: boolean;
|
isParent: boolean;
|
||||||
|
emoji?: string | null;
|
||||||
|
color?: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -21,6 +23,8 @@ export interface NewCategory {
|
|||||||
seoTitle?: string | null;
|
seoTitle?: string | null;
|
||||||
seoDescription?: string | null;
|
seoDescription?: string | null;
|
||||||
isParent?: boolean;
|
isParent?: boolean;
|
||||||
|
emoji?: string | null;
|
||||||
|
color?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fields a category update may set. Undefined = leave unchanged; parentId null = move to root. */
|
/** Fields a category update may set. Undefined = leave unchanged; parentId null = move to root. */
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface CategoryRow {
|
|||||||
seo_title: string | null;
|
seo_title: string | null;
|
||||||
seo_description: string | null;
|
seo_description: string | null;
|
||||||
is_parent: boolean;
|
is_parent: boolean;
|
||||||
|
emoji: string | null;
|
||||||
|
color: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
@@ -24,6 +26,8 @@ const UPDATABLE: ReadonlyArray<[keyof CategoryPatch, string]> = [
|
|||||||
['seoTitle', 'seo_title'],
|
['seoTitle', 'seo_title'],
|
||||||
['seoDescription', 'seo_description'],
|
['seoDescription', 'seo_description'],
|
||||||
['isParent', 'is_parent'],
|
['isParent', 'is_parent'],
|
||||||
|
['emoji', 'emoji'],
|
||||||
|
['color', 'color'],
|
||||||
];
|
];
|
||||||
|
|
||||||
export class PgCategoryRepository implements CategoryRepository {
|
export class PgCategoryRepository implements CategoryRepository {
|
||||||
@@ -57,8 +61,8 @@ export class PgCategoryRepository implements CategoryRepository {
|
|||||||
async create(input: NewCategory): Promise<Category> {
|
async create(input: NewCategory): Promise<Category> {
|
||||||
try {
|
try {
|
||||||
const result = await this.pool.query<CategoryRow>(
|
const result = await this.pool.query<CategoryRow>(
|
||||||
`INSERT INTO categories_categories (parent_id, name, slug, seo_title, seo_description, is_parent)
|
`INSERT INTO categories_categories (parent_id, name, slug, seo_title, seo_description, is_parent, emoji, color)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
input.parentId ?? null,
|
input.parentId ?? null,
|
||||||
@@ -67,6 +71,8 @@ export class PgCategoryRepository implements CategoryRepository {
|
|||||||
input.seoTitle ?? null,
|
input.seoTitle ?? null,
|
||||||
input.seoDescription ?? null,
|
input.seoDescription ?? null,
|
||||||
input.isParent ?? false,
|
input.isParent ?? false,
|
||||||
|
input.emoji ?? null,
|
||||||
|
input.color ?? null,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const row = result.rows[0];
|
const row = result.rows[0];
|
||||||
@@ -150,6 +156,8 @@ function toCategory(row: CategoryRow): Category {
|
|||||||
seoTitle: row.seo_title,
|
seoTitle: row.seo_title,
|
||||||
seoDescription: row.seo_description,
|
seoDescription: row.seo_description,
|
||||||
isParent: row.is_parent,
|
isParent: row.is_parent,
|
||||||
|
emoji: row.emoji,
|
||||||
|
color: row.color,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default async function CategoryPage({ params }: PageProps) {
|
|||||||
Categoría
|
Categoría
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-emerald-950 md:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-emerald-950 md:text-5xl">
|
||||||
{category.name}
|
{category.emoji ? `${category.emoji} ${category.name}` : category.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg leading-8 text-stone-700">
|
<p className="text-lg leading-8 text-stone-700">
|
||||||
{category.seoDescription ?? 'Descubrí productos seleccionados dentro de esta categoría.'}
|
{category.seoDescription ?? 'Descubrí productos seleccionados dentro de esta categoría.'}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface CategoryDto {
|
|||||||
url: string;
|
url: string;
|
||||||
seoTitle: string | null;
|
seoTitle: string | null;
|
||||||
seoDescription: string | null;
|
seoDescription: string | null;
|
||||||
|
emoji?: string | null;
|
||||||
|
color?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
87
work/artifacts/F-071/architect.md
Normal file
87
work/artifacts/F-071/architect.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# F-071 — Architecture: Editable emoji and color for categories
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The frontend uses **hardcoded** emoji/color maps keyed by category slug:
|
||||||
|
- `project/frontend/src/components/home/CategoriesGrid.tsx` — `icons` map + `colors` array
|
||||||
|
- `project/frontend/src/app/categories/page.tsx` — `icons` map + `colors` array
|
||||||
|
|
||||||
|
The admin category editor (`project/apps/admin/src/app/(dashboard)/categories/page.tsx`)
|
||||||
|
shows a generic 📁 folder icon for parent categories and has **no** emoji/color fields.
|
||||||
|
New parent categories therefore show the folder instead of an identifying emoji.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. Database schema (migration 030)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE categories_categories
|
||||||
|
ADD COLUMN emoji VARCHAR(10) NULL DEFAULT NULL,
|
||||||
|
ADD COLUMN color TEXT NULL DEFAULT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `emoji VARCHAR(10)`: a single emoji unicode character (with potential variation selectors). Nullable so existing categories are unaffected.
|
||||||
|
- `color TEXT`: stores a Tailwind gradient class string, e.g. `from-[#70ad47] to-[#40916C]`. Nullable. Storing the class string keeps it simple and consistent with the existing `categories/page.tsx` `colors` array format.
|
||||||
|
|
||||||
|
Both columns are nullable with NULL default → zero-downtime, no data migration needed for existing rows.
|
||||||
|
|
||||||
|
### 2. Backend domain
|
||||||
|
|
||||||
|
`project/src/modules/categories/domain/category.ts`:
|
||||||
|
- Add `emoji?: string | null` and `color?: string | null` to `Category`, `NewCategory`, `CategoryPatch`.
|
||||||
|
|
||||||
|
### 3. Backend repository
|
||||||
|
|
||||||
|
`project/src/modules/categories/infrastructure/pg-category-repository.ts`:
|
||||||
|
- Add `emoji: string | null` and `color: string | null` to `CategoryRow`.
|
||||||
|
- Add `['emoji', 'emoji']` and `['color', 'color']` to `UPDATABLE`.
|
||||||
|
- Add emoji/color to the INSERT statement.
|
||||||
|
- Map emoji/color in `toCategory()`.
|
||||||
|
|
||||||
|
### 4. Backend API routes
|
||||||
|
|
||||||
|
`project/src/modules/categories/api/categories.routes.ts`:
|
||||||
|
- Add `emoji: z.string().max(10).optional().nullable()` and `color: z.string().max(200).optional().nullable()` to `newCategorySchema`.
|
||||||
|
- Add `emoji` and `color` to `serializeCategory()` output.
|
||||||
|
|
||||||
|
### 5. Frontend types
|
||||||
|
|
||||||
|
`project/frontend/src/types/api.ts`:
|
||||||
|
- Add `emoji?: string` and `color?: string` to the `Category` interface.
|
||||||
|
|
||||||
|
### 6. Frontend — CategoriesGrid + CategoriesPage
|
||||||
|
|
||||||
|
- Replace hardcoded `icons[cat.slug] ?? '📦'` with `cat.emoji ?? fallbackIcons[cat.slug] ?? '📦'`.
|
||||||
|
- Replace hardcoded `colors[i % colors.length]` gradient with `cat.color ?? colors[i % colors.length]` — keeps existing visual behavior for categories without stored color.
|
||||||
|
|
||||||
|
### 7. Admin — types + form
|
||||||
|
|
||||||
|
`project/apps/admin/src/types/index.ts`:
|
||||||
|
- Add `emoji?: string` and `color?: string` to `Category`.
|
||||||
|
|
||||||
|
`project/apps/admin/src/app/(dashboard)/categories/page.tsx`:
|
||||||
|
- Add `emoji` and `color` to `FormState`.
|
||||||
|
- Add emoji input (text) and color input (text input for Tailwind class, with placeholder examples) to the form UI.
|
||||||
|
- Include emoji/color in the save payload.
|
||||||
|
- In `CategoryRow`, replace the hardcoded 📁 with `cat.emoji ?? '📁'`.
|
||||||
|
|
||||||
|
### 8. Storefront
|
||||||
|
|
||||||
|
`project/storefront/src/lib/api.ts`:
|
||||||
|
- Add `emoji?: string | null` and `color?: string | null` to `CategoryDto`.
|
||||||
|
|
||||||
|
`project/storefront/src/app/categoria/[slug]/page.tsx`:
|
||||||
|
- Optionally render the emoji in the header if present.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Admin form → PATCH /api/categories/:id → backend serializes → DB stores
|
||||||
|
GET /categories/tree → backend returns emoji+color → frontend renders
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **Low**: All new columns are nullable; existing categories keep working.
|
||||||
|
- The `color` field stores a CSS class string, not a sanitized color value. Admin input is validated by `z.string().max(200)`. A malicious class value is a cosmetic (not security) concern — it's rendered via `className` which React sanitizes against script injection.
|
||||||
|
- Backward compatibility: `emoji` and `color` default to `null` → UI falls back to hardcoded maps.
|
||||||
111
work/artifacts/F-071/implementer.md
Normal file
111
work/artifacts/F-071/implementer.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# F-071 — Implementer evidence: Editable emoji and color for categories
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The frontend uses **hardcoded** emoji/color maps keyed by category slug:
|
||||||
|
- `project/frontend/src/components/home/CategoriesGrid.tsx` — `icons` map + `colors` array
|
||||||
|
- `project/frontend/src/app/categories/page.tsx` — `icons` map + `colors` array
|
||||||
|
|
||||||
|
The admin category editor shows a generic 📁 folder icon for parent categories and has
|
||||||
|
**no** emoji/color editing fields. New parent categories therefore show 📁 instead of an
|
||||||
|
identifying emoji.
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
|
||||||
|
The `categories_categories` table has no `emoji` or `color` columns — emoji and color
|
||||||
|
are hardcoded in frontend JS, not stored in the database.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Migration (new)
|
||||||
|
`project/migrations/030_category_emoji_color.js` — adds `emoji VARCHAR(10)` and
|
||||||
|
`color TEXT` columns (both nullable) to `categories_categories`.
|
||||||
|
Applied to the DB successfully.
|
||||||
|
|
||||||
|
### Backend domain
|
||||||
|
`project/src/modules/categories/domain/category.ts`:
|
||||||
|
- Added `emoji?: string | null` and `color?: string | null` to `Category`, `NewCategory`.
|
||||||
|
- `CategoryPatch` inherits these via `Partial<NewCategory>`.
|
||||||
|
|
||||||
|
### Backend repository
|
||||||
|
`project/src/modules/categories/infrastructure/pg-category-repository.ts`:
|
||||||
|
- Added `emoji` and `color` to `CategoryRow`.
|
||||||
|
- Added `['emoji', 'emoji']` and `['color', 'color']` to `UPDATABLE`.
|
||||||
|
- Added emoji/color to the INSERT statement (columns + values).
|
||||||
|
- Mapped emoji/color in `toCategory()`.
|
||||||
|
|
||||||
|
### Backend API routes
|
||||||
|
`project/src/modules/categories/api/categories.routes.ts`:
|
||||||
|
- Added `emoji: z.string().max(10).optional().nullable()` and `color: z.string().max(200).optional().nullable()` to `newCategorySchema`.
|
||||||
|
- Added `emoji` and `color` to `serializeCategory()` output (propagates through `serializeTreeNode`).
|
||||||
|
|
||||||
|
### Frontend types
|
||||||
|
`project/frontend/src/types/api.ts`:
|
||||||
|
- Added `emoji?: string` and `color?: string` to the `Category` interface.
|
||||||
|
|
||||||
|
### Frontend CategoriesGrid
|
||||||
|
`project/frontend/src/components/home/CategoriesGrid.tsx`:
|
||||||
|
- `emoji = cat.emoji ?? icons[cat.slug] ?? '📦'` — uses stored emoji, falls back to slug map, then 📦.
|
||||||
|
- `colorClass = cat.color ?? colors[i % colors.length]` — uses stored color, falls back to existing array.
|
||||||
|
|
||||||
|
### Frontend CategoriesPage
|
||||||
|
`project/frontend/src/app/categories/page.tsx`:
|
||||||
|
- Same emoji/color fallback pattern as CategoriesGrid.
|
||||||
|
- `tree.map((cat, i) => {` converted to block body with `return` to support local vars.
|
||||||
|
|
||||||
|
### Storefront types
|
||||||
|
`project/storefront/src/lib/api.ts`:
|
||||||
|
- Added `emoji?: string | null` and `color?: string | null` to `CategoryDto`.
|
||||||
|
|
||||||
|
### Storefront categoria page
|
||||||
|
`project/storefront/src/app/categoria/[slug]/page.tsx`:
|
||||||
|
- Renders `category.emoji ? `${category.emoji} ${category.name}` : category.name` in the `<h1>`.
|
||||||
|
|
||||||
|
### Admin types
|
||||||
|
`project/apps/admin/src/types/index.ts`:
|
||||||
|
- Added `emoji?: string` and `color?: string` to `Category`.
|
||||||
|
|
||||||
|
### Admin categories page
|
||||||
|
`project/apps/admin/src/app/(dashboard)/categories/page.tsx`:
|
||||||
|
- Added `emoji` and `color` to `FormState` and `EMPTY_FORM`.
|
||||||
|
- Added emoji input (text with live preview) and color input (text placeholder shows Tailwind format) to the form.
|
||||||
|
- Added emoji/color to save payload (with `|| undefined` to send null when empty).
|
||||||
|
- `openEdit` loads emoji/color from the category.
|
||||||
|
- `CategoryRow` now uses `cat.emoji ?? '📁'` instead of hardcoded 📁.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `npx tsc --noEmit` — backend ✅, frontend ✅, admin ✅, storefront ✅ (all exit 0)
|
||||||
|
- `npx eslint` on all changed files — exit 0 (only pre-existing warnings)
|
||||||
|
- Migration 030 applied to DB ✅
|
||||||
|
- Backend restarted with new build
|
||||||
|
- **API test**: `PATCH /categories/:id` with `{"emoji":"🥜","color":"bg-[#70ad47]/10 text-[#70ad47]"}` → HTTP 200, response includes `"emoji":"🥜","color":"bg-[#70ad47]/10 text-[#70ad47]"` ✅
|
||||||
|
- **API test**: `GET /categories/tree` → returns emoji/color fields (null for unconfigured categories) ✅
|
||||||
|
- **API test**: `GET /admin/settings` through proxy → HTTP 200 (confirms the 500 report was a transient/auth issue, now resolved) ✅
|
||||||
|
- `./scripts/verify.sh` — exit 0
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
```
|
||||||
|
project/migrations/030_category_emoji_color.js (new)
|
||||||
|
project/src/modules/categories/domain/category.ts (modified)
|
||||||
|
project/src/modules/categories/infrastructure/pg-category-repository.ts (modified)
|
||||||
|
project/src/modules/categories/api/categories.routes.ts (modified)
|
||||||
|
project/frontend/src/types/api.ts (modified)
|
||||||
|
project/frontend/src/components/home/CategoriesGrid.tsx (modified)
|
||||||
|
project/frontend/src/app/categories/page.tsx (modified)
|
||||||
|
project/storefront/src/lib/api.ts (modified)
|
||||||
|
project/storefront/src/app/categoria/[slug]/page.tsx (modified)
|
||||||
|
project/apps/admin/src/types/index.ts (modified)
|
||||||
|
project/apps/admin/src/app/(dashboard)/categories/page.tsx (modified)
|
||||||
|
work/artifacts/F-071/implementer.md (this file)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance traceability
|
||||||
|
|
||||||
|
| Acceptance criterion | How it is met |
|
||||||
|
| -------------------- | ------------- |
|
||||||
|
| Admin can edit emoji and color per category | Form fields added to admin categories page; payload includes emoji/color; PATCH verified working |
|
||||||
|
| Frontend category cards use stored emoji and color | CategoriesGrid and categories/page.tsx use `cat.emoji ?? fallback` and `cat.color ?? fallback` |
|
||||||
|
| New parent categories show identifying emoji instead of folder | CategoryRow uses `cat.emoji ?? '📁'`; CategoriesGrid uses `cat.emoji ?? slugMap ?? 📦` |
|
||||||
|
| verify.sh is green | Exit 0 |
|
||||||
19
work/artifacts/F-071/leader-close.json
Normal file
19
work/artifacts/F-071/leader-close.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-071",
|
||||||
|
"agent": "leader",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "All gates approved. F-071 adds emoji and color fields to categories end-to-end (migration, backend, frontend, admin, storefront). Migration applied, API verified (PATCH + GET return emoji/color). Typecheck, lint, and verify.sh all green.",
|
||||||
|
"evidence": [
|
||||||
|
"work/artifacts/F-071/reviewer.json verdict=APPROVED",
|
||||||
|
"work/artifacts/F-071/security.json verdict=APPROVED",
|
||||||
|
"work/artifacts/F-071/qa.json verdict=APPROVED",
|
||||||
|
"Migration 030 applied to DB: emoji VARCHAR(10), color TEXT added to categories_categories",
|
||||||
|
"Backend rebuilt and restarted; PATCH /categories/:id with emoji+color → HTTP 200",
|
||||||
|
"GET /categories/tree returns emoji+color fields",
|
||||||
|
"GET /admin/settings through proxy → HTTP 200",
|
||||||
|
"npx tsc --noEmit on all 4 projects — exit 0",
|
||||||
|
"npx eslint on all changed files — exit 0",
|
||||||
|
"./scripts/verify.sh — exit 0, 139 features valid"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-19T17:05:00Z"
|
||||||
|
}
|
||||||
20
work/artifacts/F-071/qa.json
Normal file
20
work/artifacts/F-071/qa.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-071",
|
||||||
|
"agent": "qa",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "F-071 fully implemented and verified end-to-end. Migration applied to DB. API PATCH/GET verified returning emoji and color. Typecheck and lint green on all 4 projects. Backward compatible: nullable columns, nullable type fields, UI fallbacks to existing hardcoded maps.",
|
||||||
|
"evidence": [
|
||||||
|
"AC1 'Admin can edit emoji and color per category in the category form' — Emoji input + color input added to admin form; payload includes emoji/color; PATCH verified: PATCH /categories/:id with {emoji:'🥜', color:'bg-[#70ad47]/10 text-[#70ad47]'} → HTTP 200, response includes both fields",
|
||||||
|
"AC2 'Frontend category cards use stored emoji and color instead of hardcoded maps' — CategoriesGrid.tsx: cat.emoji ?? icons[slug] ?? 📦; cat.color ?? colors[i%len]; categories/page.tsx: same pattern",
|
||||||
|
"AC3 'New parent categories show identifying emoji instead of generic folder' — Admin CategoryRow: cat.emoji ?? 📁 (replaces hardcoded 📁); CategoriesGrid: cat.emoji ?? slugMap ?? 📦 (replaces hardcoded 📦)",
|
||||||
|
"AC4 'verify.sh is green' — exit 0, 139 features valid",
|
||||||
|
"Backward compatibility: columns are nullable NULL DEFAULT; existing categories return null for emoji/color; UI falls back to existing hardcoded maps",
|
||||||
|
"Backend typecheck: npx tsc --noEmit (project) — exit 0",
|
||||||
|
"Frontend typecheck: npx tsc --noEmit (project/frontend) — exit 0",
|
||||||
|
"Admin typecheck: npx tsc --noEmit (project/apps/admin) — exit 0",
|
||||||
|
"Storefront typecheck: npx tsc --noEmit (project/storefront) — exit 0",
|
||||||
|
"ESLint on all changed files — exit 0 (only pre-existing warnings)",
|
||||||
|
"Migration 030 applied to DB successfully"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-19T16:57:00Z"
|
||||||
|
}
|
||||||
25
work/artifacts/F-071/reviewer.json
Normal file
25
work/artifacts/F-071/reviewer.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-071",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "F-071 adds emoji and color columns to categories_categories and wires them end-to-end: backend (domain, repository, API routes, migration), frontend (types, CategoriesGrid, categories page), storefront (types, categoria page), and admin (types, form, CategoryRow). Emoji and color are nullable so existing categories are unaffected; the UI falls back to the previous hardcoded maps when no stored value exists, ensuring backward compatibility.",
|
||||||
|
"evidence": [
|
||||||
|
"Migration 030_category_emoji_color.js adds emoji VARCHAR(10) NULL and color TEXT NULL to categories_categories — applied to DB successfully",
|
||||||
|
"Backend domain: emoji? string|null and color? string|null added to Category, NewCategory, CategoryPatch",
|
||||||
|
"Backend repository: emoji/color added to CategoryRow, INSERT, UPDATABLE array, and toCategory()",
|
||||||
|
"Backend routes: emoji (max 10) and color (max 200) added to newCategorySchema (zod) and serializeCategory() output",
|
||||||
|
"Frontend types/api.ts: emoji? and color? added to Category",
|
||||||
|
"Frontend CategoriesGrid: uses cat.emoji ?? icons[slug] ?? 📦 and cat.color ?? colors[i%colors.length]",
|
||||||
|
"Frontend categories/page.tsx: same fallback pattern, map callback converted to block body",
|
||||||
|
"Storefront: CategoryDto extended; categoria page renders emoji in h1",
|
||||||
|
"Admin types: Category extended with emoji? and color?",
|
||||||
|
"Admin categories page: FormState + EMPTY_FORM + form fields + openEdit + payload + CategoryRow all updated",
|
||||||
|
"API test: PATCH /categories/:id with {emoji, color} → HTTP 200, response includes emoji and color",
|
||||||
|
"API test: GET /categories/tree returns emoji/color fields (null for unconfigured)",
|
||||||
|
"API test: GET /admin/settings → HTTP 200 (500 issue was transient)",
|
||||||
|
"npx tsc --noEmit on all 4 projects — exit 0",
|
||||||
|
"npx eslint on all changed files — exit 0 (only pre-existing warnings)",
|
||||||
|
"./scripts/verify.sh — exit 0"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-19T16:55:00Z"
|
||||||
|
}
|
||||||
18
work/artifacts/F-071/security.json
Normal file
18
work/artifacts/F-071/security.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-071",
|
||||||
|
"agent": "security",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "Change adds two nullable TEXT columns (emoji VARCHAR(10), color TEXT) to an existing table and propagates them through type definitions. No new dependencies, no new endpoints, no auth changes, no env vars, no secrets. Input validation via zod (emoji max 10 chars, color max 200 chars). The color field stores Tailwind class strings — rendered via className which is safe against XSS (React does not interpret className as HTML). Admin form sends emoji/color via existing PATCH endpoint; no new attack surface.",
|
||||||
|
"evidence": [
|
||||||
|
"No new dependencies added to any package.json",
|
||||||
|
"No new API endpoints — only fields added to existing POST/PATCH/GET /categories routes",
|
||||||
|
"No auth/authz changes — existing admin role gate applies",
|
||||||
|
"No env vars introduced",
|
||||||
|
"Input validation: z.string().max(10) for emoji, z.string().max(200) for color (zod schema in newCategorySchema)",
|
||||||
|
"color field stores Tailwind class strings — rendered via React className (no dangerouslySetInnerHTML anywhere in new code)",
|
||||||
|
"emoji is stored as text — rendered as plain text node (no HTML injection)",
|
||||||
|
"Migration columns are nullable — no data exposure risk for existing rows",
|
||||||
|
"verify.sh exit 0"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-19T16:56:00Z"
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
# Feature actual
|
# Feature actual
|
||||||
|
|
||||||
## Sin feature activa
|
## Feature activa: F-071 — Editable emoji and color for categories
|
||||||
|
|
||||||
Backlog: 138 features (138 done). Últimas features cerradas: **F-069** — Shipping method descriptions editable in admin, **F-070** — Show product attributes on frontend product detail page.
|
Backlog: 139 features (138 done, 1 in_progress). Últimas features cerradas: **F-069** — Shipping method descriptions editable in admin, **F-070** — Show product attributes on frontend product detail page.
|
||||||
|
|
||||||
|
### Problema
|
||||||
|
Las categorías usan maps hardcoded de emoji/color en el frontend (CategoriesGrid, categories/page) que no se pueden personalizar desde el admin. Además, las nuevas categorías parent muestran 📁 en lugar de su emoji identificativo.
|
||||||
|
|
||||||
|
### Solución
|
||||||
|
Añadir campos `emoji` y `color` a la tabla `categories_categories` + tipos + formulario admin + usar los valores almacenados en el frontend en lugar de maps hardcoded.
|
||||||
|
|
||||||
|
### Archivos clave
|
||||||
|
- `project/migrations/030_category_emoji_color.js` (nueva)
|
||||||
|
- `project/src/modules/categories/domain/category.ts`
|
||||||
|
- `project/src/modules/categories/infrastructure/pg-category-repository.ts`
|
||||||
|
- `project/src/modules/categories/api/categories.routes.ts`
|
||||||
|
- `project/frontend/src/types/api.ts`
|
||||||
|
- `project/frontend/src/components/home/CategoriesGrid.tsx`
|
||||||
|
- `project/frontend/src/app/categories/page.tsx`
|
||||||
|
- `project/apps/admin/src/types/index.ts`
|
||||||
|
- `project/apps/admin/src/app/(dashboard)/categories/page.tsx`
|
||||||
|
- `project/storefront/src/lib/api.ts`
|
||||||
|
- `project/storefront/src/app/categoria/[slug]/page.tsx`
|
||||||
|
|
||||||
## Servicios productivos
|
## Servicios productivos
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,13 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-070",
|
"feature_id": "F-071",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"action": "closing F-070: all gates approved",
|
"action": "closing F-071: all gates approved",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"next_agent": "reviewer",
|
"next_agent": "reviewer",
|
||||||
"waiting_for": null,
|
"waiting_for": null,
|
||||||
"updated_at": "2026-08-19T16:20:47Z",
|
"updated_at": "2026-08-19T17:04:22Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
|
||||||
"ts": "2026-08-19T14:47:25Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "regenerate cached thumbs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T14:53:28Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "center product image in column"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T14:57:14Z",
|
|
||||||
"agent": "architect",
|
|
||||||
"stage": "design",
|
|
||||||
"state": "running",
|
|
||||||
"message": "design lexical integration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T14:57:18Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "install lexical and build editor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T15:06:45Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "fix product editor loop and selection"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T15:07:47Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "add WYSIWYG to product description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T15:10:40Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "fix a11y missing id/name on search inputs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T15:19:51Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "fix image height attributes jsonb and serializeProduct"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ts": "2026-08-19T15:26:08Z",
|
"ts": "2026-08-19T15:26:08Z",
|
||||||
"agent": "implementer",
|
"agent": "implementer",
|
||||||
@@ -147,6 +91,62 @@
|
|||||||
"stage": "close",
|
"stage": "close",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "closing F-070: all gates approved"
|
"message": "closing F-070: all gates approved"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:21:09Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "close",
|
||||||
|
"state": "done",
|
||||||
|
"message": "F-070 closed and committed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:33:38Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "intake",
|
||||||
|
"state": "running",
|
||||||
|
"message": "intake: F-071 selected as active feature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:33:59Z",
|
||||||
|
"agent": "architect",
|
||||||
|
"stage": "design",
|
||||||
|
"state": "running",
|
||||||
|
"message": "designing emoji+color category fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:38:01Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "running",
|
||||||
|
"message": "implementing emoji+color category fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:54:23Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "reviewing emoji+color category implementation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:54:42Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "security review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T16:54:57Z",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "qa verification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T17:04:22Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "close",
|
||||||
|
"state": "running",
|
||||||
|
"message": "closing F-071: all gates approved"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"last_updated": "2026-08-19T09:10:00Z",
|
"last_updated": "2026-08-19T09:10:00Z",
|
||||||
|
|||||||
Reference in New Issue
Block a user