Compare commits

..

16 Commits

Author SHA1 Message Date
e65ed892f1 shipping label fix - codex 2026-03-19 21:47:26 -05:00
jason
ce2d52db53 hopeful 2026-03-19 16:38:41 -05:00
jason
39fd876d51 fixing 2026-03-19 16:34:18 -05:00
jason
0c3b2cf6fe fixes 2026-03-19 16:19:48 -05:00
jason
6423dfb91b again 2026-03-19 16:14:35 -05:00
jason
26b188de87 fix 2026-03-19 16:12:10 -05:00
jason
0b43b4ebf5 shipping fix 2026-03-19 16:06:50 -05:00
jason
3c312733ca shipping label 2 2026-03-19 16:01:32 -05:00
jason
9d54dc2ecd shipping label 2026-03-19 15:57:39 -05:00
jason
b762c70238 clean up usage guide 2026-03-19 15:37:51 -05:00
jason
9562c1cc9c usage guide 2026-03-19 13:09:29 -05:00
3eba7c5fa6 workbench 2026-03-19 07:41:06 -05:00
4949b6033f more workbench usability 2026-03-19 07:38:08 -05:00
cf54e4ba58 usability workbench 2026-03-18 23:48:14 -05:00
061057339b more 2026-03-18 23:42:30 -05:00
7b65fe06cf more workbench 2026-03-18 23:32:12 -05:00
9 changed files with 1157 additions and 41 deletions

View File

@@ -37,6 +37,16 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
- Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces - Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces
- Continued density standardization across admin diagnostics, user management, and CRM contacts, including tighter filter/forms, denser summary cards, and compact contact/account management surfaces - Continued density standardization across admin diagnostics, user management, and CRM contacts, including tighter filter/forms, denser summary cards, and compact contact/account management surfaces
- Workbench usability pass with sticky planner controls, stronger selected-row and selected-day state, clearer heatmap/day context, and more explicit dispatch-oriented action affordances - Workbench usability pass with sticky planner controls, stronger selected-row and selected-day state, clearer heatmap/day context, and more explicit dispatch-oriented action affordances
- Workbench usability depth with keyboard row navigation, enter-to-open behavior, escape-to-clear, and inline readiness/shortage/hold signal pills across planner rows and day-detail cards
- Workbench dispatch workflow depth with saved planner views, a release queue for visible ready work, queued-record visibility in the sticky control bar, and batch release directly from the workbench
- Workbench batch operation rebalance with multi-operation selection, sticky-bar batch reschedule controls, station reassignment across selected operations, and selected-operation visibility in row signals and focus context
- Workbench conflict-intelligence pass with projected batch target load, overload warnings before batch station moves, and best-alternate-station suggestions inside the sticky rebalance controls
- Workbench date-aware slot guidance using station working-day calendars and queue settings to suggest the next workable batch landing dates directly from the sticky rebalance controls
- Planning timeline now includes station day-load rollups, and Workbench slot suggestions use that server-backed per-day capacity data instead of only summary-level utilization heuristics
- Workbench now surfaces day-level capacity directly in the planner, including hot-station day counts on heatmap cells, selected-day station load breakdowns, and per-station hot-day chips in station grouping mode
- Workbench exception prioritization now scores and ranks projects, work orders, agenda rows, and dispatch exceptions by lateness, blockage, shortage, readiness, and overload pressure, with inline priority chips for faster triage
- Workbench now surfaces top-priority action lanes for `DO NOW`, `UNBLOCK`, and `RELEASE READY` records so planners can jump straight into ranked dispatch queues before working deeper lists
- Workbench action lanes now support direct follow-through from the lane cards themselves, including queue-release and the first inline build/buy/open actions without requiring a second step into the focus drawer
- Project-side milestone and work-order rollups surfaced on project list and detail pages - Project-side milestone and work-order rollups surfaced on project list and detail pages
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form - Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support - Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
@@ -93,6 +103,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
### Changed ### Changed
- Shipping-label PDFs now render inside an explicit single-page 4x6 canvas with tighter print-safe spacing and overflow-safe text wrapping to prevent second-sheet runover on label printers
- Project records now persist milestone plans directly on create/edit instead of treating schedule checkpoints as freeform notes only - Project records now persist milestone plans directly on create/edit instead of treating schedule checkpoints as freeform notes only
- Company theme colors and font now persist correctly across refresh through startup brand-profile hydration in the frontend theme provider - Company theme colors and font now persist correctly across refresh through startup brand-profile hydration in the frontend theme provider
- Demand-planning purchase-order draft generation now links sales-order lines only when the purchase item matches the originating sales item - Demand-planning purchase-order draft generation now links sales-order lines only when the purchase item matches the originating sales item

View File

@@ -112,6 +112,7 @@ This file tracks work that still needs to be completed. Shipped phase history an
### Planning and scheduling ### Planning and scheduling
- Standardize dense UI primitives and shared page shells so future Workbench, dashboard, and operational screens reuse the same cards, filter bars, empty states, and section wrappers instead of reintroducing ad hoc layout patterns
- Task dependencies, milestones, and progress updates - Task dependencies, milestones, and progress updates
- Manufacturing calendar views and deeper bottleneck visibility beyond the shipped station load and overload workbench summaries - Manufacturing calendar views and deeper bottleneck visibility beyond the shipped station load and overload workbench summaries
- Labor and machine scheduling support beyond the shipped station calendar/capacity foundation - Labor and machine scheduling support beyond the shipped station calendar/capacity foundation

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,15 @@
import puppeteer from "puppeteer"; import puppeteer, { PaperFormat } from "puppeteer";
import { env } from "../config/env.js"; import { env } from "../config/env.js";
export async function renderPdf(html: string) { interface PdfOptions {
format?: PaperFormat;
width?: string;
height?: string;
margin?: { top?: string; right?: string; bottom?: string; left?: string };
}
export async function renderPdf(html: string, options?: PdfOptions) {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
executablePath: env.PUPPETEER_EXECUTABLE_PATH, executablePath: env.PUPPETEER_EXECUTABLE_PATH,
headless: true, headless: true,
@@ -14,7 +21,10 @@ export async function renderPdf(html: string) {
await page.setContent(html, { waitUntil: "networkidle0" }); await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({ const pdf = await page.pdf({
format: "A4", format: options?.width || options?.height ? undefined : (options?.format || "A4"),
width: options?.width,
height: options?.height,
margin: options?.margin,
printBackground: true, printBackground: true,
preferCSSPageSize: true, preferCSSPageSize: true,
}); });

View File

@@ -152,29 +152,40 @@ function buildShippingLabelPdf(options: {
<html> <html>
<head> <head>
<style> <style>
@page { size: 4in 6in; margin: 8mm; } @page { size: 4in 6in; margin: 0; }
body { font-family: ${company.theme.fontFamily}, Arial, sans-serif; color: #111827; font-size: 11px; } *, *::before, *::after { box-sizing: border-box; }
.label { border: 2px solid #111827; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 12px; min-height: calc(6in - 16mm); box-sizing: border-box; } html, body { width: 4in; min-width: 4in; max-width: 4in; height: 6in; min-height: 6in; max-height: 6in; margin: 0; padding: 0; overflow: hidden; background: white; }
.row { display: flex; justify-content: space-between; gap: 12px; } body { font-family: ${company.theme.fontFamily}, Arial, sans-serif; color: #111827; font-size: 10px; line-height: 1.2; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.page { width: 4in; height: 6in; padding: 0.14in; overflow: hidden; page-break-after: avoid; break-after: avoid-page; }
.label { width: 100%; height: 100%; border: 2px solid #111827; border-radius: 10px; padding: 0.11in; display: flex; flex-direction: column; gap: 0.09in; overflow: hidden; }
.row { display: flex; justify-content: space-between; gap: 0.09in; }
.muted { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: #4b5563; } .muted { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: #4b5563; }
.brand { border-bottom: 2px solid ${company.theme.primaryColor}; padding-bottom: 10px; } .brand { border-bottom: 2px solid ${company.theme.primaryColor}; padding-bottom: 0.09in; }
.brand h1 { margin: 0; font-size: 18px; color: ${company.theme.primaryColor}; } .brand-row { align-items: flex-start; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 10px; } .brand-company { flex: 1; min-width: 0; padding-right: 0.06in; }
.stack { display: flex; flex-direction: column; gap: 4px; } .brand h1 { margin: 0; font-size: 16px; line-height: 1.05; color: ${company.theme.primaryColor}; overflow-wrap: anywhere; }
.barcode { border: 2px solid #111827; border-radius: 10px; padding: 8px; text-align: center; font-family: monospace; font-size: 18px; letter-spacing: 0.18em; } .shipment-number { width: 1.25in; flex: 0 0 1.25in; text-align: right; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 0.08in; min-width: 0; }
.stack { display: flex; flex-direction: column; gap: 3px; }
.barcode { border: 2px solid #111827; border-radius: 8px; padding: 0.08in; text-align: center; font-family: monospace; font-size: 16px; line-height: 1; letter-spacing: 0.15em; }
.strong { font-weight: 700; } .strong { font-weight: 700; }
.big { font-size: 16px; font-weight: 700; } .big { font-size: 15px; line-height: 1.05; font-weight: 700; }
.footer { text-align: center; font-size: 9px; color: #4b5563; overflow-wrap: anywhere; }
.reference-text { margin-top: 6px; overflow-wrap: anywhere; word-break: break-word; }
.block > div[style="margin-top:6px;"] { overflow-wrap: anywhere; word-break: break-word; }
div[style="text-align:center; font-size:10px; color:#4b5563;"] { text-align: center; font-size: 9px; color: #4b5563; overflow-wrap: anywhere; }
</style> </style>
</head> </head>
<body> <body>
<div class="page">
<div class="label"> <div class="label">
<div class="brand"> <div class="brand">
<div class="row"> <div class="row brand-row">
<div> <div class="brand-company">
<div class="muted">From</div> <div class="muted">From</div>
<h1>${escapeHtml(company.companyName)}</h1> <h1>${escapeHtml(company.companyName)}</h1>
</div> </div>
<div style="text-align:right;"> <div class="shipment-number">
<div class="muted">Shipment</div> <div class="muted">Shipment</div>
<div class="big">${escapeHtml(shipment.shipmentNumber)}</div> <div class="big">${escapeHtml(shipment.shipmentNumber)}</div>
</div> </div>
@@ -217,7 +228,7 @@ function buildShippingLabelPdf(options: {
</div> </div>
</body> </body>
</html> </html>
`); `, { width: "4in", height: "6in", margin: { top: "0", right: "0", bottom: "0", left: "0" } });
} }
function buildBillOfLadingPdf(options: { function buildBillOfLadingPdf(options: {

View File

@@ -2,6 +2,7 @@ import type {
GanttLinkDto, GanttLinkDto,
GanttTaskDto, GanttTaskDto,
PlanningReadinessState, PlanningReadinessState,
PlanningStationDayLoadDto,
PlanningStationLoadDto, PlanningStationLoadDto,
PlanningTaskActionDto, PlanningTaskActionDto,
PlanningTimelineDto, PlanningTimelineDto,
@@ -94,6 +95,15 @@ type StationAccumulator = {
workingDays: number[]; workingDays: number[];
}; };
type StationDayAccumulator = {
stationId: string;
dateKey: string;
plannedMinutes: number;
actualMinutes: number;
operationCount: number;
capacityMinutes: number;
};
function clampProgress(value: number) { function clampProgress(value: number) {
return Math.max(0, Math.min(100, Math.round(value))); return Math.max(0, Math.min(100, Math.round(value)));
} }
@@ -199,6 +209,23 @@ function createStationLoad(record: StationAccumulator): PlanningStationLoadDto {
}; };
} }
function createStationDayLoad(record: StationDayAccumulator): PlanningStationDayLoadDto {
const capacityMinutes = Math.max(record.capacityMinutes, 1);
const utilizationPercent = Math.round((record.plannedMinutes / capacityMinutes) * 100);
const actualUtilizationPercent = Math.round((record.actualMinutes / capacityMinutes) * 100);
return {
stationId: record.stationId,
dateKey: record.dateKey,
plannedMinutes: record.plannedMinutes,
actualMinutes: record.actualMinutes,
capacityMinutes,
utilizationPercent,
actualUtilizationPercent,
operationCount: record.operationCount,
overloaded: utilizationPercent > 100,
};
}
function buildProjectTask( function buildProjectTask(
project: PlanningProjectRecord, project: PlanningProjectRecord,
projectWorkOrders: PlanningWorkOrderRecord[], projectWorkOrders: PlanningWorkOrderRecord[],
@@ -492,6 +519,7 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
} }
const stationAccumulators = new Map<string, StationAccumulator>(); const stationAccumulators = new Map<string, StationAccumulator>();
const stationDayAccumulators = new Map<string, StationDayAccumulator>();
for (const workOrder of openWorkOrders) { for (const workOrder of openWorkOrders) {
const insight = workOrderInsights.get(workOrder.id); const insight = workOrderInsights.get(workOrder.id);
for (const operation of workOrder.operations) { for (const operation of workOrder.operations) {
@@ -525,7 +553,22 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
current.lateCount += 1; current.lateCount += 1;
} }
for (let cursor = startOfDay(operation.plannedStart).getTime(); cursor <= startOfDay(operation.plannedEnd).getTime(); cursor += DAY_MS) { for (let cursor = startOfDay(operation.plannedStart).getTime(); cursor <= startOfDay(operation.plannedEnd).getTime(); cursor += DAY_MS) {
current.dayKeys.add(dateKey(new Date(cursor))); const currentDate = new Date(cursor);
const currentDateKey = dateKey(currentDate);
current.dayKeys.add(currentDateKey);
const dayAccumulatorKey = `${operation.station.id}:${currentDateKey}`;
const dayAccumulator = stationDayAccumulators.get(dayAccumulatorKey) ?? {
stationId: operation.station.id,
dateKey: currentDateKey,
plannedMinutes: 0,
actualMinutes: 0,
operationCount: 0,
capacityMinutes: Math.max(operation.station.dailyCapacityMinutes, 60) * Math.max(operation.station.parallelCapacity, 1),
};
dayAccumulator.plannedMinutes += operation.plannedMinutes;
dayAccumulator.actualMinutes += operation.actualMinutes;
dayAccumulator.operationCount += 1;
stationDayAccumulators.set(dayAccumulatorKey, dayAccumulator);
} }
stationAccumulators.set(operation.station.id, current); stationAccumulators.set(operation.station.id, current);
} }
@@ -537,6 +580,14 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
} }
return left.stationCode.localeCompare(right.stationCode); return left.stationCode.localeCompare(right.stationCode);
}); });
const stationDayLoads = [...stationDayAccumulators.values()]
.map(createStationDayLoad)
.sort((left, right) => {
if (left.dateKey !== right.dateKey) {
return left.dateKey.localeCompare(right.dateKey);
}
return left.stationId.localeCompare(right.stationId);
});
const stationLoadById = new Map(stationLoads.map((load) => [load.stationId, load])); const stationLoadById = new Map(stationLoads.map((load) => [load.stationId, load]));
const tasks: GanttTaskDto[] = []; const tasks: GanttTaskDto[] = [];
@@ -863,5 +914,6 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
}) })
.slice(0, 12), .slice(0, 12),
stationLoads, stationLoads,
stationDayLoads,
}; };
} }

View File

@@ -95,10 +95,23 @@ export interface PlanningStationLoadDto {
lateCount: number; lateCount: number;
} }
export interface PlanningStationDayLoadDto {
stationId: string;
dateKey: string;
plannedMinutes: number;
actualMinutes: number;
capacityMinutes: number;
utilizationPercent: number;
actualUtilizationPercent: number;
operationCount: number;
overloaded: boolean;
}
export interface PlanningTimelineDto { export interface PlanningTimelineDto {
tasks: GanttTaskDto[]; tasks: GanttTaskDto[];
links: GanttLinkDto[]; links: GanttLinkDto[];
summary: PlanningSummaryDto; summary: PlanningSummaryDto;
exceptions: PlanningExceptionDto[]; exceptions: PlanningExceptionDto[];
stationLoads: PlanningStationLoadDto[]; stationLoads: PlanningStationLoadDto[];
stationDayLoads: PlanningStationDayLoadDto[];
} }

96
test-puppeteer.js Normal file
View File

@@ -0,0 +1,96 @@
import puppeteer from 'puppeteer';
import fs from 'fs';
const html = `
<html>
<head>
<style>
@page { size: 4in 6in; margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
html, body { width: 4in; height: 6in; margin: 0; padding: 0.5in; overflow: hidden; }
body { font-family: Arial, sans-serif; color: #111827; font-size: 11px; }
.label { border: 2px solid #111827; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 12px; height: 100%; overflow: hidden; }
.row { display: flex; justify-content: space-between; gap: 12px; }
.muted { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: #4b5563; }
.brand { border-bottom: 2px solid #ed8936; padding-bottom: 10px; }
.brand h1 { margin: 0; font-size: 18px; color: #ed8936; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 10px; }
.stack { display: flex; flex-direction: column; gap: 4px; }
.barcode { border: 2px solid #111827; border-radius: 10px; padding: 8px; text-align: center; font-family: monospace; font-size: 18px; letter-spacing: 0.18em; }
.strong { font-weight: 700; }
.big { font-size: 16px; font-weight: 700; }
</style>
</head>
<body>
<div class="label">
<div class="brand">
<div class="row">
<div>
<div class="muted">From</div>
<h1>Message Point Media</h1>
</div>
<div style="text-align:right;">
<div class="muted">Shipment</div>
<div class="big">SHP-00003</div>
</div>
</div>
</div>
<div class="block">
<div class="muted">Ship To</div>
<div class="stack" style="margin-top:8px;">
<div class="strong">Northwind Fabrication</div>
<div>42 Assembly Ave</div>
<div>Milwaukee, WI 53202</div>
<div>USA</div>
</div>
</div>
<div class="row">
<div class="block" style="flex:1;">
<div class="muted">Service</div>
<div class="big" style="margin-top:6px;">GROUND</div>
</div>
<div class="block" style="width:90px;">
<div class="muted">Pkgs</div>
<div class="big" style="margin-top:6px;">1</div>
</div>
</div>
<div class="row">
<div class="block" style="flex:1;">
<div class="muted">Sales Order</div>
<div class="strong" style="margin-top:6px;">SO-00002</div>
</div>
<div class="block" style="width:110px;">
<div class="muted">Ship Date</div>
<div class="strong" style="margin-top:6px;">N/A</div>
</div>
</div>
<div class="block">
<div class="muted">Reference</div>
<div style="margin-top:6px;">FG-CTRL-BASE · Control Base Assembly</div>
</div>
<div class="barcode">
*SHP-00003*
</div>
<div style="text-align:center; font-size:10px; color:#4b5563;">Carrier pending · Tracking pending</div>
</div>
</body>
</html>
`;
async function run() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({
format: "A4",
printBackground: true,
preferCSSPageSize: true,
});
fs.writeFileSync('/tmp/test-label.pdf', pdf);
console.log("PDF generated at /tmp/test-label.pdf");
} finally {
await browser.close();
}
}
run();

71
usage_guide.md Normal file
View File

@@ -0,0 +1,71 @@
# CODEXIUM: End-to-End Workflow Guide
This guide walks through the core operational workflow in CODEXIUM, starting from capturing a customer request in a Quote, all the way to shipping the final product.
## 1. Create a Quote
The process begins when a customer requests pricing.
1. Navigate to the **Quotes** module.
2. Click to create a **New Quote**.
3. Select the Customer using the searchable lookup.
4. Add **Quote Lines** by searching for the requested Inventory SKUs. The default price from the item master will automatically populate.
5. Add any necessary discounts, freight, and notes.
6. The Quote can go through an approval process. Once the customer accepts the terms, proceed to the next step.
## 2. Convert to Sales Order
Once the Quote is accepted, it becomes firm demand.
1. Open the approved **Quote**.
2. Use the action menu to **Convert to Sales Order**.
3. This creates a new Sales Order record, carrying over all lines, pricing, and customer information.
4. The Sales Order is now the authoritative commercial record for this demand.
## 3. Create a Project
For complex or long-running deliveries, create a Project to track the execution.
1. Navigate to the **Projects** module.
2. Click **New Project** and select the same Customer.
3. Define the project scope, priority, due dates, and owner.
4. Set up high-level **Milestones** to track progress on deliverables.
## 4. Assign Quote and Sales Order to Project
Link the commercial documents to the execution tracker.
1. Open the newly created **Project**.
2. In the project details, link the original **Quote** and the active **Sales Order**.
3. *Note: You can also link the Project from within the Quote and Sales Order detail pages. This reverse-link ensures that quote conversion automatically carries the project context into the Sales Order.*
4. The Project dashboard now provides visibility into the commercial value and linked deliverables.
## 5. Determine Manufacturing & Supply Requirements (Demand Planning)
With the Sales Order firm and linked to a Project, determine what needs to be made or bought.
1. Navigate to the **Workbench** module, where the **Demand Planning** view is available for the Sales Order.
2. The system runs a **Multi-level BOM explosion** against the Sales Order lines, netting against current stock and open supply (existing POs/MOs).
3. The system will generate **Build/Buy recommendations** for any shortages.
## 6. Issue Purchase Orders (POs)
Fulfill the "Buy" recommendations.
1. From the Demand Planning recommendations, use the planner-assisted conversion to draft **Purchase Orders** for the required buyout items and raw materials.
2. The POs will automatically peg back to the source Sales Order and carry the Project context.
3. Preferred vendors from the inventory item master are selected by default.
4. Send the PO PDFs to vendors and manage receiving in the **Purchase Orders** and **Warehouse** modules.
## 7. Issue Manufacturing Orders (MOs/Work Orders)
Fulfill the "Build" recommendations.
1. Again, from the Demand Planning recommendations, convert the "Build" shortages into **Work Orders**.
2. The Work Orders define the execution plan (Operations/Stations) based on the item's routing templates in the **Manufacturing** module.
3. Provide the Work Order to the shop floor.
4. As operators log labor and issue materials against the Work Order, the costs roll up, and final completion posts the finished goods to inventory, making them available for shipment.
## 8. Assign Shipping
Once production is complete, deliver the goods.
1. Navigate to the **Shipments** module and create a **New Shipment**.
2. Link the Shipment directly to the **Sales Order**.
3. The system shows the ordered vs. picked vs. remaining quantities.
4. Execute **Shipment Picking**, which pulls stock from specific warehouse locations and posts the inventory issue transactions.
5. Update logistics details: Carrier, Service Level, Tracking Number, and Package Count.
6. Generate branded logistics PDFs directly from the Shipment: **Packing Slips, Shipping Labels, and Bills of Lading**.
7. The Shipment can also be seen from the linked Project, closing the loop on the delivery lifecycle.