1What is this page, in one paragraph
This page reads every closed Fishbowl manufacturing order since Jan 2024, looks at what fabric was actually consumed and what labor the operators actually clocked on their Fishbowl Time iPads, and turns that into numbers like "the archer backer costs us $0.97 in sewing labor per square foot to build." You then use those real numbers — plus a margin — to generate a quote price. It replaces our old hardcoded estimates (educated guesses from 2022) with actual data from 801 completed jobs.
Think of it as three things in one:
- A reporting tool — "how much did it really cost us to make this product last time?"
- A pricing tool — "what should we quote a new customer for a similar product?"
- A learning tool — as more MOs close, the rates get tighter and more accurate on their own.
2Reading the Learning tab (default landing)
The Learning tab is the front door. It answers one question: "is the system actually getting better as customers submit configurations and we link the resulting MO/WOs?"
The tab is built around the multi-configurator platform. Today TruSim is the only configurator that links orders, but you'll see scaffolded "not connected yet" cards for Canvas, Industrial Laundry, and TruTruss — they'll activate automatically the moment those configurators start writing configurator_links with their own group prefixes.
Per-vertical card at the top:
- Linked — total customer configurations linked to a Fishbowl MO/WO/SO from the order drawer.
- w/ actuals — of those linked, how many have had Fishbowl actuals pulled. Until actuals are pulled the order can't contribute to learning.
- Current MAE — mean absolute error of estimated total cost vs actual total cost, across all orders with actuals. Color-coded: green ≤15%, amber 15–40%, red >40%.
- Trend arrow — split the orders chronologically by when actuals came in; compare first-half MAE to second-half MAE. Down arrow + "getting better" means the system is learning. Need at least 6 samples before a trend can be detected.
Cumulative MAE chart: each dot is one order with actuals pulled, ordered chronologically. Yellow dots are over-estimates (we said it'd cost more than it did), blue dots are under-estimates. The blue line is the running mean of |variance %|. If the line slopes down as you go right, the system is learning. Flat means we're stuck; up means something is regressing — usually a new construction type that the engine hasn't seen yet.
Per-group breakdown: each "group" is a construction family — TruSim today uses keys like SCREEN_TRIPLE, SCREEN_GC340/60BK, DRAPE_V15, ARCHER_BACKER. Confidence colors mirror everywhere else: green = 10+ samples, yellow = 3–9, red = 1–2. Bias % is signed — positive means the engine over-estimates (you'd quote too high), negative means under-estimates (you'd quote too low and lose margin).
Recent feedback events: the last six orders that had actuals pulled. Each row shows the MO, customer, group, time the actuals came in, and the dollar variance. Useful for catching "why did this jump?" after a refresh.
Next milestones: progress bars showing which group is closest to crossing the next confidence threshold. "Pull actuals on 2 more drape v15 orders to reach yellow" is a directly actionable nudge.
3Where the data comes from
Every number on this page traces back to Fishbowl. The chain:
- Fishbowl MySQL (read-only). We connect over the Cloudflare Tunnel at port 4080 and query the
mo, moitem, part, woitem, and wo tables directly. No writes. The connection physically cannot modify Fishbowl.
- For each closed MO (statusId 50, 55, or 60) we pull: the finished goods made, every material consumed (fabric yards, hook/loop, webbing, grommets), and every labor line from the connected work orders.
- Fabric → square feet: Fishbowl stores fabric quantities in yards. To convert to square feet we need the roll width — which lives in our Parts Master (Firestore
canvas_config_parts collection). So 11.1 yd of GC340/60BK (60" wide) → 11.1 × 60/12 × 3 = 166.5 sqft.
- Labor cost comes from
woitem.cost, which is the real per-operator dollar amount Fishbowl Time writes when someone clocks out on the iPad. If Maria sewed for 2 hours at $29/hr, that row says $58.00. Not a flat $25/hr average.
- Results land in Firestore under
production_intelligence/. Two collections you'll see referenced on this page:
mo_extracts/{mo_id} — one document per processed MO, holding the full extracted detail (audit trail).
part_rates/{pn_safe} — one document per finished-good SKU, holding the aggregated/learned rates.
What we do not touch
We do not modify anything in Fishbowl. We do not modify MOs, WOs, parts, or customer records. This whole page is read-only against Fishbowl.
4Reading the Parts tab
This is the main list of every finished-good SKU we've learned rates for.
Each row is one finished-good SKU (like CGC340BAK-IGD — the archer backer) and shows how we've been building that product historically.
| Column | What it means |
| Part # | Fishbowl's part number for the finished good. Always unique. |
| Description | The part description from Fishbowl, verbatim. |
| MOs | How many closed manufacturing orders we have for this SKU. More = more reliable rates. |
| Conf | Confidence badge: green for 10+ MO samples (trust these), yellow for 3–9 (useful but still learning), red for 1–2 (don't bet the farm on these yet). |
| Fabric $/sqft | Median material cost to make one square foot of this product. Based on all MOs combined. |
| Sew min/sqft | Median sewing time per square foot. Includes all trim sewing — see the Glossary. |
| Last built | Most recent completion date for a MO of this SKU. |
Click any row to open a detail modal with:
- All five labor metrics (sewing, spreading, packing, grommet, CNC) with median + average + stddev + sample count
- An "outlier count" for each metric (samples that were more than 2 standard deviations from the mean)
- A table of every past MO that fed into this SKU's rates — click-through audit trail.
Why only medians show in the table
The median is the middle value — it's robust against outliers. The average gets yanked around by one weird MO. For example, if we have 9 MOs that sewed at 0.5–0.7 min/sqft and one that was posted wrong at 72 min/sqft, the average is now 7.8 min/sqft (useless) but the median is still 0.6 min/sqft (correct). The detail modal shows both so you can spot noise; the main table shows medians.
5Using Quick Quote to price a job
Type in dimensions → get a cost breakdown and suggested price.
Fill in the inputs (left side):
- SqFt (required)
- Total finished-fabric square footage for the job. If you're building 4 tarps at 50 sqft each, enter 200.
- Trim linear in
- Total inches of velcro, binding, webbing, patches, or flaps you're adding. Used to pick the right "trim-heavy" vs "low-trim" rate bucket. Leave 0 for a plain flat product.
- Grommets
- Total count of grommets to install on the job. Each grommet adds ~0.75 min of labor.
- CNC pieces
- Number of pieces being cut on the CNC machine. Currently uses a per-sqft proxy (learned per-piece rate is coming later).
- Similar SKU
- If this job is like a SKU we've made before, type the part number. The engine will use THAT SKU's specific rates instead of the global blend. This is the most accurate option.
- Vertical
- Which product family this belongs to. Determines the default margin: Golf 70%, Backer 50%, Canvas 50%, Laundry 60%, etc. (Edit defaults on the Margins tab.)
- Margin override
- Leave blank to use the vertical's default margin. Type a number (e.g.
55) to force a 55% margin for this specific quote.
The right side shows:
- Cost breakdown — one line per material and labor code, showing how each number was calculated (e.g. "110.4 min @ $29/hr").
- Build cost (bold at bottom of breakdown) — what it costs Canwil to produce.
- Quote price at X% margin (in the blue box) — what to charge the customer.
- "At other margins" table — same quote recomputed at 40/50/60/70% for comparison.
- Rate source label at the top right — tells you which rates were used: "SKU-specific rates (CGC340BAK-IGD)" = most accurate; "Global blend — heavy_trim" = fallback.
Best-accuracy workflow
If you're about to quote a product we've built before: type its part number in "Similar SKU". The autocomplete only offers SKUs with ≥3 MO samples (yellow+ confidence). When you pick one, Quick Quote uses that specific SKU's learned rates — dramatically more accurate than the global blend.
6The Margins tab
Controls the markup added on top of build cost to produce a customer quote.
Margins work in a three-layer cascade. When quoting, we check for the most specific rule first and fall back if not found:
- Per-customer override — e.g. "Inside Golf Design gets 58% on all their products" or "Inside Golf Design gets 65% specifically on CGSP-IGD." (Edited in Firestore for now — UI comes later.)
- Per-part override — e.g. "All CGSP-IGD screens always quote at 65%, regardless of customer." (Edited in Firestore for now.)
- Vertical default — e.g. "All golf screens default to 70%." This is editable on the Margins tab.
To edit a vertical default: type the new percentage into any row, hit Save Changes. Changes take effect immediately on the next quote.
Margin is gross-margin, not markup
Margin here means gross profit margin: the portion of the sale price that is profit. If margin is 70%, then 70% of the quote is profit and 30% is cost. The math: quote_price = build_cost ÷ (1 − 0.70) = build_cost × 3.33. So a $100 cost at 70% margin quotes at $333. Not $170.
7Reading Global Rates
Blended rates across ALL parts. Your fallback when no SKU-specific rates exist.
This tab shows two big tables:
Per-SqFt Rates — labor time and fabric cost per square foot, averaged across every build in the system. Each row:
- Median (big bold number) — the trustworthy middle value. Use this.
- Average (grey) — shown for reference. Distorted by outliers.
- Stddev — how much spread there is in the samples. High stddev = the rate varies a LOT between different products.
- Samples — how many MO slices contributed this metric.
- Outliers — how many samples were >2σ from the mean. Higher = more noise in the data.
- Hardcoded — the old number the configurators used before this page existed.
- Delta — how far off the hardcoded value is from reality. green within 20%, amber 20-50% off, red more than 50% off.
Labor Hourly Rates — the actual dollars-per-hour operators are being paid, pulled from woitem.cost. The hardcoded $25/hr is wrong for pretty much every operation — this table shows the real numbers.
When to use global rates vs per-SKU rates
Global rates are a fallback for products we've never made, or rough back-of-envelope estimates. For real quoting, always prefer per-SKU rates (enter a Similar SKU on Quick Quote) — they're 3-5× more accurate because they're tuned to one specific product's construction.
8Reading Run History
An audit log of every time the learning engine processed MOs.
Each row is one extraction run. Every time you hit "Run Now" (or the nightly cron fires, once that's built), a new row appears.
| Column | What it means |
| Run ID | Unique identifier for that run. A dry badge means it was a dry-run (computed but didn't save). |
| Started | How long ago this run finished. |
| Duration | How long the run took (seconds or minutes). |
| Processed | How many new MOs got extracted in that batch. |
| Errors | How many MOs failed. Non-zero = something weird on those MOs; click into /api/production-intelligence/history?limit=20 for stack traces. |
| Parts Updated | How many finished-good SKU rate-documents got recomputed after that batch. |
| Params | What query params were used (limit, since_mo_id, etc.). |
What "Processed: 0" means: we're caught up — no new closed MOs since the last run.
9What the "Run Now" button does
Hit it to pull in the latest MOs without waiting for the nightly cron.
Clicking ▶ Run Now (next 100) does this, in order:
- Looks at the "Last Processed MO" counter in the top banner (e.g. 1182).
- Queries Fishbowl for the next 100 closed MOs with
dateCompleted ≥ 2024-01-01 and id > last_processed.
- For each one, extracts all the material + labor detail as described in Section 2.
- Saves each extract to Firestore under
mo_extracts/{mo_id}.
- Recomputes the aggregated per-SKU rates and the global blend.
- Updates "Last Processed MO" for next time.
It takes about 20 seconds per 100 MOs. If "Processed: 0" comes back, you're up to date.
To reprocess everything from scratch
In the Firestore console, delete production_intelligence/meta.last_processed_mo_id, then hit Run Now in batches. The writes are idempotent (keyed by mo_id), so reprocessing doesn't corrupt anything — it just burns compute.
10The quote math — worked example
One real example from end to end so you can check the arithmetic.
Scenario: Quote an archer backer job — 166 sqft of GC340 fabric, 12 grommets, some webbing trim, standard construction. Target margin: 50%.
Rates the engine uses (from part_rates/CGC340BAK-IGD):
Step 1 — materials
fabric cost/sqft = $0.20 (median, 2 samples)
→ fabric total = 166 × $0.20 = $33.20
Step 2 — labor (per-sqft codes)
sewing: 0.99 min/sqft × 166 sqft = 164.3 min ÷ 60 × $29/hr = $79.41
spreading: 0.05 min/sqft × 166 sqft = 8.3 min ÷ 60 × $29/hr = $4.01
packing: 0.02 min/sqft × 166 sqft = 3.3 min ÷ 60 × $22/hr = $1.21
Step 3 — grommets (per-ea proxy)
12 grommets × 0.75 min/ea = 9 min ÷ 60 × $22/hr = $3.30
Step 4 — subtotal + buffer
subtotal = 33.20 + 79.41 + 4.01 + 1.21 + 3.30 = $121.13
buffer (10%) = 121.13 × 0.10 = $12.11
BUILD COST = 121.13 + 12.11 = $133.24
Step 5 — margin → quote price
margin = 50%
quote_price = build_cost ÷ (1 − 0.50) = 133.24 ÷ 0.50 = $266.48
Sanity check: at $266.48 quoted minus $133.24 cost, Canwil makes $133.24 profit — which is exactly 50% of the sale price. That's what "50% gross margin" means.
11Gotchas & how to read suspicious numbers
- A rate looks way too high or too low. Click into the SKU's detail modal, scroll to the sample MOs table. You can usually spot the outlier — a 0.1-sqft rework or a MO where someone posted wrong. Small-sample SKUs (red confidence) are especially prone to this.
- A part has $0 fabric cost. Its MOs didn't consume anything classified as fabric. Usually means the roll width isn't in the Parts Master, or it's a pass-through item. Fix: add roll width to
canvas_config_parts for that part.
- Global rates vs per-SKU rates disagree wildly. That's normal and correct. The global blend mixes a flat-fabric reroll with a grommeted-and-velcroed cover. Per-SKU is always more trustworthy.
- Sewing time includes trim sewing. We don't have a separate "LaborTrimSewing" code in Fishbowl — attaching velcro IS sewing. Products with lots of velcro naturally show higher sew min/sqft. That's a feature, not a bug — the trim-heavy vs low-trim bucketing on the Global Rates tab exists specifically to handle this.
- You don't see a SKU you expected. Check: (a) has it been built in a closed MO since 2024-01-01? (b) does the MO have
woitem rows (labor entries from the iPad)? If no labor data, no rates. (c) click Run Now to pick up any recently-closed MOs.
- Hourly rates seem low (e.g. $1.78/hr). Bad Fishbowl Time posting — someone probably entered minutes instead of hours. Flagged as an outlier, excluded from the median, but still visible as the `min` column on Global Rates. Look at median (~$22/hr), not min/max.
12Glossary
| MO | Manufacturing Order. A job in Fishbowl that produces one or more finished goods. |
| WO | Work Order. The labor side of an MO. Operators clock time against WOs from their iPads. |
| SO | Sales Order. The customer-facing quote/invoice. |
| Finished good | The thing we make (e.g. CGC340BAK-IGD = an archer backer). Each MO has ≥1. |
| SKU | Part number for a finished good. Used interchangeably with "Part #" on this page. |
| Fabric sqft | Yards consumed × (roll width inches / 12) × 3. The standard conversion. |
| Per-sqft rate | A cost or time value divided by the job's fabric sqft. Our main unit of comparison. |
| Heavy trim / Low trim | Products with trim_yards/sqft > 0.05 are "heavy-trim" (sew slower). Below that = "low-trim" (sew faster). Used to bucket the global blend. |
| Median | The middle value in a sorted list. Robust against outliers. Use this. |
| Stddev | Standard deviation. Measures spread. Low stddev = consistent rate across MOs; high stddev = rate varies a lot. |
| Outlier | A sample more than 2 standard deviations from the mean. Still counted but flagged. |
| Confidence | How many MOs support a SKU's rates. 10+ green, 3-9 yellow, 1-2 red. |
| Extract | The processed data for one MO, stored in Firestore. Source of truth for the learning engine. |
| Buffer (10%) | A markup on build cost to cover shop overhead (utilities, supervision, indirect labor) before margin is applied. |
| Gross margin | Sale price minus cost, divided by sale price. Expressed as %. 50% margin ≠ 50% markup. |
| Parts Master | Our Firestore canvas_config_parts collection. Holds customer-facing labels, categories, and roll widths. |
| LaborSewing | Includes all sewing — edges, attaching trim, velcro, patches, hems. Not just panel joins. |
| LaborSpreading | Time spent spreading fabric for cutting. Per sqft. |
| LaborPacking | Folding, bagging, labeling the finished product. |
| LaborGrommet | Installing grommets. Per-ea. |
| LaborCNC | CNC cutting time. Currently per-sqft proxy. |
| LaborSlitting | Slitting rolls to width. Mostly for laundry-prep operations. |
13What's coming next
In rough order of planned delivery:
- Linking drawings to MOs. The real unlock. Recreate past products in the Laundry/TruSim/Canvas configurators, link each to its actual MO + WO + SO. Then the engine can correlate construction features (grommet count, seam count, perimeter) to actual labor — not just "per-sqft" but "per-feature." That's when quotes become genuinely parametric.
- Per-part and per-customer margin UI. Right now those overrides live only in Firestore. UI coming so you can set them without dev involvement.
- Nightly cron. Right now Run Now is manual. Will fire automatically every night at 3am ET.
- Wiring customer configurators to use these rates. Laundry, TruSim, and Canvas still use the old hardcoded coefficients when customers generate live quotes. Once enough training data is in, we swap in Quick Quote's engine — and the whole company benefits from the learned rates automatically.
- Per-piece CNC and per-grommet learned rates. Both currently use proxy rates. Will become learned as training volume grows.
- Trim material cost as separate line. Currently baked into fabric_cost_per_sqft for SKU-specific quotes. Will pull out as explicit material cost eventually.
Questions? Ping Nik. The engine runs entirely on staging/production Railway apps, Firestore, and Fishbowl MySQL.