diff --git a/docs/architecture/frontend-boundary.md b/docs/architecture/frontend-boundary.md
index f4d64e7..19bd217 100644
--- a/docs/architecture/frontend-boundary.md
+++ b/docs/architecture/frontend-boundary.md
@@ -25,8 +25,9 @@
## UI Workflow Coverage
- Kitchen board lanes with work-item detail and station coverage
-- Board event feed derived from the loaded lane state
+- Board event feed derived from the loaded lane state and shared restaurant lifecycle progression
- Claim, release, transition, and priority operator actions
- Latest operator result and recent action history
- Session-expired handling with reauthentication guidance
- Protected route shell for board, operator actions, and session inspection
+- Kitchen transitions are presented as order progression toward floor handoff and payment eligibility.
diff --git a/docs/runbooks/local-development.md b/docs/runbooks/local-development.md
index 14838af..bbb9775 100644
--- a/docs/runbooks/local-development.md
+++ b/docs/runbooks/local-development.md
@@ -26,6 +26,7 @@ npm run dev
## Available Screens
- `/board`: lane-based kitchen board and board event feed
+- `/board`: lane-based kitchen board tied to restaurant order progression and handoff readiness
- `/actions`: claim, release, transition, and priority operator controls
- `/session`: current Thalos session profile payload
diff --git a/docs/runbooks/testing.md b/docs/runbooks/testing.md
index 5f4fe75..54417c5 100644
--- a/docs/runbooks/testing.md
+++ b/docs/runbooks/testing.md
@@ -17,7 +17,7 @@ npm run test:ci
- `src/api/client.test.ts`: runtime-config precedence and fallback behavior.
- `src/api/dashboardApi.test.ts`: endpoint path/query composition for board and operator action flows.
- `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback.
-- `src/App.test.tsx`: central login screen, board lane loading, operator actions, and session-expired reauthentication guidance.
+- `src/App.test.tsx`: central login screen, lifecycle-aware board loading, operator actions, and session-expired reauthentication guidance.
## Notes
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 1e13ced..f13ccfb 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -67,7 +67,7 @@ describe('Kitchen Ops App', () => {
summary: '2 active kitchen lanes',
lanes: [
{
- lane: 'Cooking',
+ lane: 'Ready',
items: [
{
workItemId: 'WORK-1',
@@ -75,7 +75,7 @@ describe('Kitchen Ops App', () => {
ticketId: 'TICK-1',
tableId: 'T-01',
station: 'Grill',
- state: 'Cooking',
+ state: 'Ready',
priority: 2,
claimedBy: 'chef-a',
etaMinutes: 12
@@ -92,8 +92,9 @@ describe('Kitchen Ops App', () => {
fireEvent.click(screen.getByRole('button', { name: 'Load Kitchen Board' }));
await waitFor(() => expect(loadDashboard).toHaveBeenCalledWith('demo-context'));
- expect(await screen.findByText('Lane: Cooking')).toBeInTheDocument();
- expect(await screen.findByText('Cooking: WORK-1 at Grill is Cooking')).toBeInTheDocument();
+ expect(await screen.findByText('Lifecycle Stage: Ready')).toBeInTheDocument();
+ expect(screen.getByText(/Kitchen lanes are a live view of restaurant orders/)).toBeInTheDocument();
+ expect(await screen.findByText('Ready: ORD-1 / TICK-1 at Grill is Ready and the order is ready for handoff to floor service')).toBeInTheDocument();
});
it('runs operator actions from the actions route', async () => {
@@ -114,7 +115,7 @@ describe('Kitchen Ops App', () => {
orderId: 'ORD-1',
ticketId: 'TICK-1',
previousState: 'Cooking',
- currentState: 'Ready',
+ currentState: 'Served',
transitioned: true,
error: null
});
@@ -130,10 +131,14 @@ describe('Kitchen Ops App', () => {
expect(screen.getAllByText('chef-a').length).toBeGreaterThan(0);
fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'ORD-1' } });
+ fireEvent.change(screen.getAllByPlaceholderText('Context Id')[2], { target: { value: 'demo-context' } });
fireEvent.click(screen.getByRole('button', { name: 'Transition Work Item' }));
await waitFor(() => expect(transitionKitchenWorkItem).toHaveBeenCalledTimes(1));
- expect(await screen.findByText('Cooking -> Ready')).toBeInTheDocument();
+ expect(transitionKitchenWorkItem).toHaveBeenCalledWith(
+ expect.objectContaining({ contextId: 'demo-context', orderId: 'ORD-1' })
+ );
+ expect(await screen.findByText('Cooking -> Served (the order is complete and the check can move to payment)')).toBeInTheDocument();
});
it('shows reauthentication guidance when board loading returns session expired', async () => {
diff --git a/src/App.tsx b/src/App.tsx
index 8b7643f..9a1eeb0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -78,7 +78,7 @@ const boardColumns = [
{
title: 'State',
dataIndex: 'state',
- render: (value: string) => {value}
+ render: (value: string) => {value}
},
{ title: 'Priority', dataIndex: 'priority' },
{ title: 'Claimed By', render: (_: unknown, record: KitchenBoardItem) => record.claimedBy ?? 'Unclaimed' },
@@ -326,9 +326,18 @@ function KitchenOpsShell() {
{boardPayload.contextId}
{boardPayload.summary}
{boardPayload.availableStations.join(', ')}
+
+ Kitchen lanes are a live view of restaurant orders moving from accepted work into preparation,
+ handoff readiness, and final service completion.
+
{boardPayload.lanes.map((lane) => (
-
+ {lane.lane}}
+ >
pagination={false}
rowKey={(record) => record.workItemId}
@@ -349,7 +358,10 @@ function KitchenOpsShell() {
- lane.items.map((item) => `${lane.lane}: ${item.workItemId} at ${item.station} is ${item.state}`)
+ lane.items.map(
+ (item) =>
+ `${lane.lane}: ${item.orderId} / ${item.ticketId} at ${item.station} is ${item.state} and ${workflowProgressHint(item.state)}`
+ )
)}
renderItem={(item) => {item}}
/>
@@ -407,9 +419,18 @@ function KitchenOpsShell() {
+
+
@@ -523,9 +544,14 @@ function ActionDescriptions({ event }: { event: ActionEvent }) {
{event.response.orderId}
{event.response.ticketId}
- {event.response.previousState}
- {event.response.currentState}
+
+ {event.response.previousState}
+
+
+ {event.response.currentState}
+
{String(event.response.transitioned)}
+ {workflowProgressHint(event.response.currentState)}
{event.response.error ?? 'None'}
);
@@ -556,12 +582,58 @@ function getActionSummary(event: ActionEvent): string {
}
if (event.kind === 'transition') {
- return `${event.response.previousState} -> ${event.response.currentState}`;
+ return `${event.response.previousState} -> ${event.response.currentState} (${workflowProgressHint(event.response.currentState)})`;
}
return event.response.summary;
}
+function workflowTagColor(state: string): string {
+ switch (state.toLowerCase()) {
+ case 'queued':
+ case 'accepted':
+ return 'blue';
+ case 'cooking':
+ case 'preparing':
+ return 'orange';
+ case 'ready':
+ case 'readyforpickup':
+ return 'cyan';
+ case 'served':
+ case 'delivered':
+ return 'green';
+ case 'paid':
+ return 'purple';
+ case 'blocked':
+ case 'failed':
+ case 'canceled':
+ return 'red';
+ default:
+ return 'default';
+ }
+}
+
+function workflowProgressHint(state: string): string {
+ switch (state.toLowerCase()) {
+ case 'queued':
+ case 'accepted':
+ return 'waiting for a kitchen operator to start preparation';
+ case 'cooking':
+ case 'preparing':
+ return 'the kitchen is actively preparing this order';
+ case 'ready':
+ case 'readyforpickup':
+ return 'the order is ready for handoff to floor service';
+ case 'served':
+ case 'delivered':
+ return 'the order is complete and the check can move to payment';
+ case 'paid':
+ return 'the restaurant workflow is fully closed';
+ default:
+ return 'the kitchen workflow is still progressing';
+ }
+}
+
function providerLabel(provider: IdentityProvider): string {
if (provider === 0 || provider === '0' || provider === 'InternalJwt') {
return 'Internal JWT';
diff --git a/src/api/dashboardApi.ts b/src/api/dashboardApi.ts
index c9cc923..1e81b26 100644
--- a/src/api/dashboardApi.ts
+++ b/src/api/dashboardApi.ts
@@ -57,6 +57,7 @@ export type TransitionKitchenWorkItemRequest = {
ticketId: string;
targetState: string;
updatedBy: string;
+ contextId?: string;
};
export type TransitionKitchenWorkItemResponse = {