feat(F-177): completed feature
This commit is contained in:
@@ -7013,6 +7013,68 @@
|
||||
"description": "Create admin header notification module for pending and shipped orders unchanged for more than 24 hours",
|
||||
"priority": "high",
|
||||
"risk": "med",
|
||||
"status": "done",
|
||||
"created_at": "2026-08-22",
|
||||
"gates": {
|
||||
"reviewer": true,
|
||||
"security": true,
|
||||
"qa": true,
|
||||
"close": true
|
||||
},
|
||||
"completed_at": "2026-08-22T16:54:41Z"
|
||||
},
|
||||
{
|
||||
"id": "F-178",
|
||||
"type": "fix",
|
||||
"title": "Make POS EAN search unique and Enter add item",
|
||||
"description": "Exact EAN search must show only one product and Enter must add one unit to the current order",
|
||||
"priority": "high",
|
||||
"risk": "low",
|
||||
"status": "pending",
|
||||
"created_at": "2026-08-22",
|
||||
"gates": {
|
||||
"reviewer": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "F-179",
|
||||
"type": "fix",
|
||||
"title": "Fix POS sale Internal Server Error",
|
||||
"description": "POST /api/pos/sales returns HTTP 500 when completing a TPV sale",
|
||||
"priority": "high",
|
||||
"risk": "high",
|
||||
"status": "pending",
|
||||
"created_at": "2026-08-22",
|
||||
"gates": {
|
||||
"reviewer": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "F-180",
|
||||
"type": "fix",
|
||||
"title": "Accept decimal euros in POS discount",
|
||||
"description": "POS discount input must accept 3,50 or 3.50 as EUR instead of raw cents",
|
||||
"priority": "high",
|
||||
"risk": "med",
|
||||
"status": "pending",
|
||||
"created_at": "2026-08-22",
|
||||
"gates": {
|
||||
"reviewer": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "F-181",
|
||||
"type": "feature",
|
||||
"title": "Fix POS customer search and add customer creation",
|
||||
"description": "POS customer search returns no clients and cashier must be able to create a customer from the register",
|
||||
"priority": "high",
|
||||
"risk": "med",
|
||||
"status": "pending",
|
||||
"created_at": "2026-08-22",
|
||||
"gates": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { AuthProvider, useAuth } from '@/features/auth/components/AuthProvider';
|
||||
import { OrderNotifications } from '@/components/OrderNotifications';
|
||||
import { visibleNavItems, type NavItem } from '@/lib/permissions';
|
||||
import type { Role } from '@/types';
|
||||
|
||||
@@ -216,6 +217,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
||||
<div className="hidden lg:block">
|
||||
<SidebarToggle expanded={!desktopCollapsed} onToggle={toggleDesktop} mode="desktop" />
|
||||
</div>
|
||||
{user.role === 'admin' && <OrderNotifications />}
|
||||
</header>
|
||||
<main className="w-full px-4 py-6 sm:px-6 lg:px-10 lg:py-8">{children}</main>
|
||||
</div>
|
||||
|
||||
118
project/apps/admin/src/components/OrderNotifications.tsx
Normal file
118
project/apps/admin/src/components/OrderNotifications.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ordersApi, type StaleOrderNotification } from '@/lib/api-client';
|
||||
|
||||
const POLL_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
function ageLabel(hours: number): string {
|
||||
if (hours < 48) return `${hours} h sin cambiar`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} ${days === 1 ? 'día' : 'días'} sin cambiar`;
|
||||
}
|
||||
|
||||
function money(cents: number): string {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(cents / 100);
|
||||
}
|
||||
|
||||
export function OrderNotifications() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [items, setItems] = useState<StaleOrderNotification[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setError('');
|
||||
try {
|
||||
const data = await ordersApi.staleNotifications();
|
||||
setItems(data.items);
|
||||
setTotal(data.total);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'No se pudieron cargar las notificaciones');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const timer = window.setInterval(() => void load(), POLL_INTERVAL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
void load();
|
||||
const close = (event: MouseEvent) => {
|
||||
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
|
||||
};
|
||||
const key = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', close);
|
||||
document.addEventListener('keydown', key);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', close);
|
||||
document.removeEventListener('keydown', key);
|
||||
};
|
||||
}, [open, load]);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="relative ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-label={`Notificaciones de pedidos${total ? `: ${total} pendientes` : ''}`}
|
||||
aria-expanded={open}
|
||||
className="relative inline-flex h-10 w-10 items-center justify-center rounded-xl border border-gray-200 bg-white text-gray-600 shadow-sm hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2D6A4F]"
|
||||
>
|
||||
<svg aria-hidden="true" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.85 23.85 0 0 0 5.454-1.31A8.97 8.97 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.97 8.97 0 0 1-2.312 6.022 23.85 23.85 0 0 0 5.455 1.31m5.714 0a24.26 24.26 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||
</svg>
|
||||
{total > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 min-w-5 rounded-full bg-[#E76F51] px-1 text-center text-[11px] font-bold leading-5 text-white">
|
||||
{total > 99 ? '99+' : total}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<section aria-label="Pedidos sin cambios durante más de 24 horas" className="absolute right-0 top-12 z-50 w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-gray-900">Pedidos para revisar</h2>
|
||||
<p className="text-xs text-gray-500">Pendientes o enviados sin cambios en 24 h</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => void load()} className="text-xs font-medium text-[#2D6A4F] hover:underline">Actualizar</button>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{loading && <p className="p-6 text-center text-sm text-gray-500">Cargando…</p>}
|
||||
{!loading && error && <p className="p-6 text-center text-sm text-red-600">{error}</p>}
|
||||
{!loading && !error && items.length === 0 && <p className="p-6 text-center text-sm text-gray-500">No hay pedidos atrasados.</p>}
|
||||
{!loading && !error && items.map((item) => (
|
||||
<Link key={item.id} href={`/orders/${item.id}`} onClick={() => setOpen(false)} className="block border-b border-gray-100 px-4 py-3 last:border-0 hover:bg-gray-50">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-gray-900">Pedido #{item.id.slice(0, 8)}</p>
|
||||
<p className="truncate text-xs text-gray-500">{item.customerEmail || 'Cliente sin email'}</p>
|
||||
</div>
|
||||
<span className={`shrink-0 rounded-full px-2 py-1 text-[11px] font-semibold ${item.state === 'PENDING' ? 'bg-amber-100 text-amber-800' : 'bg-blue-100 text-blue-800'}`}>
|
||||
{item.state === 'PENDING' ? 'Pendiente' : 'Enviado'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-between text-xs">
|
||||
<span className="font-medium text-[#E76F51]">{ageLabel(item.ageHours)}</span>
|
||||
<span className="text-gray-600">{money(item.totalCents)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{total > items.length && <Link href="/orders" onClick={() => setOpen(false)} className="block border-t border-gray-100 px-4 py-3 text-center text-sm font-medium text-[#2D6A4F] hover:bg-gray-50">Ver los {total} pedidos</Link>}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +93,15 @@ export const productsApi = {
|
||||
|
||||
// ── Orders ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface StaleOrderNotification {
|
||||
id: string;
|
||||
state: 'PENDING' | 'SHIPPED';
|
||||
totalCents: number;
|
||||
customerEmail: string | null;
|
||||
stateChangedAt: string;
|
||||
ageHours: number;
|
||||
}
|
||||
|
||||
export const ordersApi = {
|
||||
list: (params?: { limit?: number; offset?: number; status?: string; q?: string }) => {
|
||||
const sp = new URLSearchParams();
|
||||
@@ -128,6 +137,10 @@ export const ordersApi = {
|
||||
),
|
||||
history: (id: string) =>
|
||||
api.get<{ items: import('@/types').OrderHistoryEvent[] }>(`/api/orders/${id}/history`),
|
||||
staleNotifications: (hours = 24, limit = 20) =>
|
||||
api.get<{ total: number; thresholdHours: number; items: StaleOrderNotification[] }>(
|
||||
`/api/orders/notifications/stale?hours=${hours}&limit=${limit}`,
|
||||
),
|
||||
};
|
||||
|
||||
// ── Customers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
28
project/migrations/051_order_state_changed_at.js
Normal file
28
project/migrations/051_order_state_changed_at.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = (pgm) => {
|
||||
pgm.sql(`
|
||||
ALTER TABLE orders_orders
|
||||
ADD COLUMN IF NOT EXISTS state_changed_at timestamptz;
|
||||
|
||||
UPDATE orders_orders
|
||||
SET state_changed_at = updated_at
|
||||
WHERE state_changed_at IS NULL;
|
||||
|
||||
ALTER TABLE orders_orders
|
||||
ALTER COLUMN state_changed_at SET DEFAULT now(),
|
||||
ALTER COLUMN state_changed_at SET NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS orders_orders_stale_state_idx
|
||||
ON orders_orders (state, state_changed_at)
|
||||
WHERE state IN ('PENDING', 'SHIPPED');
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = (pgm) => {
|
||||
pgm.sql(`
|
||||
DROP INDEX IF EXISTS orders_orders_stale_state_idx;
|
||||
ALTER TABLE orders_orders DROP COLUMN IF EXISTS state_changed_at;
|
||||
`);
|
||||
};
|
||||
@@ -180,6 +180,59 @@ export async function registerOrdersRoutes(
|
||||
return reply.send({ items: items.map(serializeOrder), total });
|
||||
});
|
||||
|
||||
const staleNotificationsSchema: FastifySchema = {
|
||||
tags: ['Orders'],
|
||||
summary: 'List stale order notifications (admin)',
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hours: { type: 'integer', minimum: 1, maximum: 168, default: 24 },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
|
||||
},
|
||||
},
|
||||
response: { 401: errorSchema, 403: errorSchema },
|
||||
};
|
||||
app.get('/orders/notifications/stale', { schema: staleNotificationsSchema }, async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
const query = request.query as { hours?: string; limit?: string };
|
||||
const hours = Math.min(Math.max(Number(query.hours ?? 24) || 24, 1), 168);
|
||||
const limit = Math.min(Math.max(Number(query.limit ?? 20) || 20, 1), 50);
|
||||
const result = await deps.pool.query<{
|
||||
id: string;
|
||||
state: 'PENDING' | 'SHIPPED';
|
||||
total_cents: number;
|
||||
customer_email: string | null;
|
||||
state_changed_at: Date;
|
||||
age_hours: number;
|
||||
total_count: number;
|
||||
}>(
|
||||
`SELECT o.id, o.state, o.total_cents, u.email AS customer_email,
|
||||
o.state_changed_at,
|
||||
FLOOR(EXTRACT(EPOCH FROM (now() - o.state_changed_at)) / 3600)::int AS age_hours,
|
||||
COUNT(*) OVER()::int AS total_count
|
||||
FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
WHERE o.state IN ('PENDING', 'SHIPPED')
|
||||
AND o.state_changed_at <= now() - ($1::int * interval '1 hour')
|
||||
ORDER BY o.state_changed_at ASC
|
||||
LIMIT $2`,
|
||||
[hours, limit],
|
||||
);
|
||||
return reply.send({
|
||||
total: result.rows[0]?.total_count ?? 0,
|
||||
thresholdHours: hours,
|
||||
items: result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
state: row.state,
|
||||
totalCents: row.total_cents,
|
||||
customerEmail: row.customer_email,
|
||||
stateChangedAt: row.state_changed_at.toISOString(),
|
||||
ageHours: row.age_hours,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
const getOrderAdminSchema: FastifySchema = {
|
||||
tags: ['Orders'],
|
||||
summary: 'Get order (admin)',
|
||||
|
||||
@@ -232,7 +232,12 @@ export class PgOrderRepository implements OrderRepository {
|
||||
|
||||
async updateState(id: string, state: OrderState, trackingNumber?: string, courier?: string): Promise<OrderView | undefined> {
|
||||
const result = await this.pool.query<OrderRow>(
|
||||
`UPDATE orders_orders SET state = $2, tracking_number = COALESCE($3, tracking_number), courier = COALESCE($4, courier), updated_at = now()
|
||||
`UPDATE orders_orders
|
||||
SET state = $2,
|
||||
tracking_number = COALESCE($3, tracking_number),
|
||||
courier = COALESCE($4, courier),
|
||||
state_changed_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1 RETURNING *`,
|
||||
[id, state, trackingNumber ?? null, courier ?? null],
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function registerPaymentsRoutes(
|
||||
if (!row) throw new OrderNotFoundError();
|
||||
if (!ALLOWED_TRANSITIONS[row.state]?.includes(state)) throw new OrderNotFoundError();
|
||||
await deps.pool.query(
|
||||
'UPDATE orders_orders SET state = $2, updated_at = now() WHERE id = $1',
|
||||
'UPDATE orders_orders SET state = $2, state_changed_at = now(), updated_at = now() WHERE id = $1',
|
||||
[id, state],
|
||||
);
|
||||
const refreshed = await deps.pool.query<{
|
||||
|
||||
7
work/artifacts/F-177/architect.md
Normal file
7
work/artifacts/F-177/architect.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# F-177 — Architecture
|
||||
|
||||
- Add `orders_orders.state_changed_at`, initialized from `updated_at`, indexed for monitored states.
|
||||
- Every state transition updates both `updated_at` and `state_changed_at`; non-state edits only update `updated_at`.
|
||||
- Admin endpoint `GET /orders/notifications/stale?hours=24&limit=20`: RBAC admin, fixed monitored states, parameterized threshold, total + items.
|
||||
- Header client component polls every 5 minutes, refreshes when opened, closes on Escape/outside click, and links to `/orders/:id`.
|
||||
- Bell badge caps visual text at `99+`; dropdown differentiates pending and shipped.
|
||||
3
work/artifacts/F-177/documenter.md
Normal file
3
work/artifacts/F-177/documenter.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Admin order notifications
|
||||
|
||||
The bell in the Admin header lists orders that have remained `Pending` or `Shipped` for more than 24 hours without a state transition. The badge is the total number of stale orders. Open the bell to review age, customer and amount; selecting an entry opens that order. Data refreshes every five minutes and whenever the panel opens.
|
||||
8
work/artifacts/F-177/implementer.md
Normal file
8
work/artifacts/F-177/implementer.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# F-177 — Implementer
|
||||
|
||||
- Migration 051 adds indexed `state_changed_at`, backfilled from `updated_at`.
|
||||
- Repository and payment state transitions now update that timestamp; edits unrelated to state do not reset the 24 h clock.
|
||||
- New admin-only `GET /orders/notifications/stale` returns PENDING/SHIPPED orders older than threshold, total count, customer, amount and elapsed hours.
|
||||
- Header bell includes count badge, accessible dropdown, status/age/amount, direct links, empty/error/loading states, 5-minute polling, manual refresh, Escape and outside-click closing.
|
||||
|
||||
Evidence: migration applied; project typecheck PASS; order tests 7/7; Admin Next build PASS; authenticated endpoint HTTP 200. Controlled acceptance row at 25 h returned total=1/state=PENDING/ageHours=25, then database row was restored.
|
||||
1
work/artifacts/F-177/leader-close.json
Normal file
1
work/artifacts/F-177/leader-close.json
Normal file
@@ -0,0 +1 @@
|
||||
{"feature_id":"F-177","agent":"leader","stage":"close","verdict":"APPROVED","checks":[{"item":"reviewer/security/qa approved","ok":true},{"item":"migration applied","ok":true},{"item":"controlled 25-hour runtime acceptance","ok":true},{"item":"verify.sh green","ok":true}],"issues":[]}
|
||||
1
work/artifacts/F-177/qa.json
Normal file
1
work/artifacts/F-177/qa.json
Normal file
@@ -0,0 +1 @@
|
||||
{"feature_id":"F-177","agent":"qa","stage":"qa_gate","verdict":"APPROVED","checks":[{"item":"25-hour PENDING order appears","ok":true},{"item":"fresh/no qualifying orders excluded","ok":true},{"item":"endpoint total and age correct","ok":true},{"item":"Admin build","ok":true},{"item":"orders regression tests 7/7","ok":true},{"item":"verify.sh","ok":true}],"issues":[]}
|
||||
1
work/artifacts/F-177/reviewer.json
Normal file
1
work/artifacts/F-177/reviewer.json
Normal file
@@ -0,0 +1 @@
|
||||
{"feature_id":"F-177","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","checks":[{"item":"dedicated state transition timestamp","ok":true},{"item":"PENDING and SHIPPED threshold query","ok":true},{"item":"responsive accessible header dropdown","ok":true},{"item":"tests/build/runtime","ok":true}],"issues":[]}
|
||||
1
work/artifacts/F-177/security.json
Normal file
1
work/artifacts/F-177/security.json
Normal file
@@ -0,0 +1 @@
|
||||
{"feature_id":"F-177","agent":"security","stage":"security_gate","verdict":"APPROVED","checks":[{"item":"admin role required","ok":true},{"item":"hours and limit bounded","ok":true},{"item":"SQL values parameterized","ok":true},{"item":"no sensitive data beyond existing admin order access","ok":true}],"issues":[]}
|
||||
@@ -1,3 +1,3 @@
|
||||
# F-176 — Fix POS existing product search
|
||||
# F-177 — Add stale order notifications to admin header
|
||||
|
||||
POS search currently executes stale SQL against nonexistent variant fields (`v.name`, `v.active`, `p.active`, `s.quantity`, `pp.price_cents`, category-by-variant), then hides HTTP 500 as an empty result. Align search/EAN/SKU queries with current catalog, pricing, inventory and category schemas. Barcode text must support exact and partial EAN lookup.
|
||||
Create an admin-only stale-order endpoint and header bell. Notify when an order remains in `PENDING` or `SHIPPED` for at least 24 hours since its last state transition. Show badge count, dropdown details, elapsed time and direct links to each order. Refresh automatically and on demand.
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"feature_id": "F-176",
|
||||
"feature_id": "F-177",
|
||||
"stage": "close",
|
||||
"agent": "leader",
|
||||
"action": "Close POS existing product search fix",
|
||||
"action": "Close admin stale-order notifications",
|
||||
"state": "running",
|
||||
"next_agent": "leader",
|
||||
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||
"updated_at": "2026-08-22T16:49:23Z",
|
||||
"updated_at": "2026-08-22T16:54:21Z",
|
||||
"timeline": [
|
||||
{
|
||||
"ts": "2026-08-22T16:45:39Z",
|
||||
"ts": "2026-08-22T16:49:42Z",
|
||||
"agent": "leader",
|
||||
"stage": "intake",
|
||||
"state": "running",
|
||||
"message": "Diagnose POS product search for existing barcode"
|
||||
"message": "Define stale pending/shipped order notifications"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:46:57Z",
|
||||
"ts": "2026-08-22T16:50:13Z",
|
||||
"agent": "architect",
|
||||
"stage": "design",
|
||||
"state": "running",
|
||||
"message": "Align POS lookup SQL with current catalog schema"
|
||||
"message": "Design state-age endpoint and admin header bell"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:47:05Z",
|
||||
"ts": "2026-08-22T16:50:24Z",
|
||||
"agent": "implementer",
|
||||
"stage": "build",
|
||||
"state": "running",
|
||||
"message": "Repair POS product lookup queries and visible errors"
|
||||
"message": "Implement stale-order endpoint, state timestamp and header dropdown"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:48:42Z",
|
||||
"ts": "2026-08-22T16:53:33Z",
|
||||
"agent": "reviewer",
|
||||
"stage": "review_gate",
|
||||
"state": "running",
|
||||
"message": "Review current-schema POS search repair"
|
||||
"message": "Review state-age semantics and header component"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:48:51Z",
|
||||
"ts": "2026-08-22T16:53:42Z",
|
||||
"agent": "security",
|
||||
"stage": "security_gate",
|
||||
"state": "running",
|
||||
"message": "Audit POS search authorization and SQL parameters"
|
||||
"message": "Audit notification endpoint RBAC and query bounds"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:49:02Z",
|
||||
"ts": "2026-08-22T16:53:51Z",
|
||||
"agent": "qa",
|
||||
"stage": "qa_gate",
|
||||
"state": "running",
|
||||
"message": "Validate partial barcode and exact EAN searches"
|
||||
"message": "Validate 24-hour stale-order notification acceptance"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:49:12Z",
|
||||
"ts": "2026-08-22T16:54:10Z",
|
||||
"agent": "documenter",
|
||||
"stage": "document",
|
||||
"state": "running",
|
||||
"message": "Document repaired POS lookup behavior"
|
||||
"message": "Document stale-order notification behavior"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-22T16:49:23Z",
|
||||
"ts": "2026-08-22T16:54:21Z",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"state": "running",
|
||||
"message": "Close POS existing product search fix"
|
||||
"message": "Close admin stale-order notifications"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user