Added secure, native Shopify order importing with guided location and product mapping, durable synchronization, reviewable unmatched items, and localized management tools.
Added a capability-gated Square integration with guided location mapping, background catalog and sales synchronization, resilient catalog matching, retail performance insights, and Square-only product, sell-through, and inventory reporting.
Added shared AI-assisted product matching for Shopify and Square while keeping tenant authorization, confidence checks, and saved mappings under Demi's control.
Changed
Made the public ROI calculator substantially faster to load by separating it from the authenticated application shell while preserving global feedback and analytics behavior.
Improved recipe editing and costing so large recipes avoid repeated cost requests, packaging changes refresh confirmed costs, and shared recipe categories remain visible in search.
Improved ingredient procurement displays so measured case contents, normalized costs, package titles, and price history remain consistent without exposing compatibility-only pack values.
Improved privacy-safe signup and activation-email visibility so acquisition, workspace activation, onboarding, and delivery outcomes can be diagnosed without blocking valid signups.
Fixed
Fixed integration OAuth popup, reconnect, onboarding, backfill, webhook recovery, and retry races across Square, Shopify, and QuickBooks Online.
Fixed Square gross-profit costing for legacy and partially costed products while keeping production, inventory, wastage, and portfolio classifications conservative.
Fixed recipe-creation metadata errors, delayed optimistic saves after navigation, execution cutoff fallbacks, and stale recipe costing responses.
Fixed inventory reporting after physical counts so later production, usage, sales, waste, and counted-batch expiry are reconciled in the correct order.
Fixed backend cold starts that could stall on missing Typesense synonym collections during tenant or location changes.
Security
Protected Square and Shopify credentials, OAuth state, webhooks, worker dispatch, migrations, and provider mappings with tenant-bound encryption, signed or authenticated callbacks, durable idempotency, and tenant-safe data boundaries.
Release
5.4.2
Changed
Improved Kitchen Production List inventory edits so saved changes reconcile against authoritative data sooner without waiting for unrelated downstream work.
Improved invoice vendor auto-detection so an exact vendor identity wins over similar longer vendor names while keeping the import flow automatic.
Fixed
Fixed Update Costs Only invoice imports so existing tenant-scoped ingredients are treated as updates instead of being incorrectly reported as newly added.
Prevented update-cost imports and replays from creating duplicate scoped ingredients or procurements when cached lookups or package identity are incomplete.
Fixed overlapping or superseded Kitchen Production List refreshes so stale work cannot overwrite newer inventory values, including after navigation or tenant changes.
Release
5.4.1
Added
Added an optional Kitchen Production List print setting that repeats the company, worklist, and date header on every PDF page while preserving the existing single-header default.
Changed
Improved Kitchen Production Lists so each tenant, location, and workflow remembers its own date, keeps hydrated rows visible during validation, and refreshes only affected cached views after order, recipe, or production-setup changes.
Improved AI Test Kitchen generation speed and consistency, clarified prepared-batch yields, and made saved recipe costing recover automatically from stale cached recipe data.
Improved the public developer portal with clean documentation URLs, reliable page delivery, privacy-conscious analytics, and clearer agent connection guidance.
Fixed
Fixed Kitchen Production List routing and recalculation so product-backed recipes stay in Production or Execution, stale work cannot overwrite newer results, and scoped refreshes preserve authoritative demand, stock, expiry, PAR, and rounding behavior.
Prevented ingredient and recipe workflows from creating duplicate Units, strengthened invalid Unit handling, and preserved procurement references for reviewed cleanup operations.
Prevented impossible generated yields from publishing invalid nutrition while preserving valid recipe, costing, method, and insight results.
Fixed customer-location edits for existing customers that do not have a customer-level address.
Release
5.4.0
Added
Introducing AI Test Kitchen (Beta): authorized Growth and Enterprise teams can turn natural-language briefs into costed, nutritionally analyzed products and sub-recipes, refine results in chat, and save drafts for review.
Expanded Demi MCP so approved AI agents can securely help manage recipes, ingredients, inventory, purchasing, receiving, wastage, invoices, customers, locations, and product lineups.
Changed
Improved AI Test Kitchen with better reuse of verified library items, accurate nested sub-recipes, realistic yields, partial-cost guidance, per-serving pricing, and a faster, clearer review and save experience.
Improved Demi MCP with clearer operator guidance and more dependable, tenant-scoped agent workflows.
Improved signup, Google sign-in, invitation activation, and remembered MFA devices with safer account linking and fewer repeated challenges.
Fixed
Fixed AI Test Kitchen issues affecting legacy records, incomplete costs, servings and yields, interrupted sessions, and draft saving.
Fixed re-inviting previously removed team members and prevented invalid negative procurement or selling prices.
Release
5.3.13
Added
Added modern multi-factor authentication with passkeys, authenticator apps, SMS, recovery codes, trusted devices, required enrollment, and profile security controls.
Added a public Recipe and Ingredient Imports guide with current authentication, examples, webhook guidance, plan names, and navigation.
Changed
Expanded company, location, customer, and signup address handling worldwide with country-aware validation and more precise timezones.
Improved customer product-lineup assignment, delivery-day controls, optional payment terms, and invalid nutrition display handling.
Refined the public ROI calculator with consistent savings drivers, a clearer email-success state, and improved responsive behavior.
Improved Ingredient Imports by cleaning supply names, preserving raw source identity, handling catch-weight costing correctly, and retaining review-safe diagnostics.
Fixed
Fixed Ingredient Price Change reports and exports so ingredients without procurements no longer appear as empty rows.
Fixed seafood imports so authoritative ingredient identity takes precedence and materially different species fall back to review instead of updating the wrong item.
Fixed MFA enrollment and challenge flows across passkeys, authenticator apps, SMS, recovery codes, and trusted devices, including legacy WebAuthn users and clearer localized errors.
Security
Hardened login, session, tenant selection, public API key scoping, by-ID mutations, sensitive side effects, and customer user administration to fail closed across tenant boundaries.
Hardened Google OAuth with browser-bound state, PKCE, secure short-lived cookies, and generic error handling.
Redacted email and other authentication-sensitive query values from analytics URL fields while preserving safe product analytics.
Changed
Hardened AI Insights substitution presentation with deterministic role/state gates, versioned direct-versus-guided decisions, verified backend cost authority, and concise operator-facing summaries.
Release
5.3.12
Added
Added an Ingredient Price Change report with category, status, item-type, and vendor filters; operator KPI summaries; procurement-level comparisons; and PDF/Excel exports.
Changed
Restored the segmented content-language toggle for read-only Ingredient and Recipe views while preserving the profile language selector.
Improved Ingredient Import review navigation so the selected import run, review banner, and sort context are retained reliably.
Fixed
Fixed Ingredient Imports so touched ingredients receive the required allergen and category enrichment, with caches and search results refreshed after completion.
Fixed seafood allergen declarations so Canadian and U.S. locations use the appropriate persisted regulatory relations and label statements.
Fixed matched procurement imports so distinct vendor codes cannot overwrite the same target and ambiguous seafood matches fall back to review instead of updating the wrong ingredient.
Release
5.3.11
Added
Added Spanish, Canadian French, and Japanese language options across Demi, with saved profile preferences and localized account, settings, navigation, workflow, and supporting app content.
Added the Demi MCP v1 integration for approved AI-agent hosts, including secure OAuth connection and consent, bounded company/location discovery, operational briefings, catalog and reporting reads, and private export workflows.
Changed
Improved profile saves with immediate in-progress feedback and duplicate-save protection.
Improved USDA nutrition matching by filtering procurement noise, requiring stronger food-name matches, and normalizing nutrient amounts and density conversions more accurately.
Improved the Recipe Importer success and review experience with clearer status, row interactions, error messaging, and safer prepared-starch normalization.
Improved ingredient import reviews with exact import-run scoping, a dedicated exit action, and 24-hour price-change badges for procurement-only updates.
Improved Orders, invoices, customers, locations, and Kitchen Production actions with current QuickBooks and print icons, and removed an irrelevant Orders search toggle.
Reduced the initial application bundle by loading translation dictionaries only when needed, while retaining English fallback behavior.
Fixed
Fixed confirmed Recipe and Ingredient Importer records so saved recipes and ingredients become active and available in their libraries.
Fixed super-admin email and MFA login flows, including OTP persistence, verification, resend-code consistency, and completion of token issuance.
Fixed recipe relation webhooks so ingredient and nested-recipe create, update, and delete events resolve the correct relation records without cross-tenant ID collisions.
Removed OAuth codes, keys, OTPs, and tokens from analytics URL properties and cleaned authorization codes from the login URL after capture.
Release
5.3.10
Changed
Improved inventory purchase orders, goods receiving, physical counts, receiving lists, dashboard recommendations, and wastage controls with more consistent Demi buttons, badges, icons, spacing, and table behavior.
Improved public changelog publishing guidance so versioned releases use the documented GCP Cloud Build / Cloud Run path plus targeted CDN invalidation instead of GitHub Actions or local Docker.
Fixed
Fixed internal ordering and product-lineup refresh behavior so newly assigned products become orderable immediately after successful lineup saves.
Fixed cache invalidation and confirmed-state handling across customer lineups, internal production orders, purchase orders, inventory receiving, physical counts, sales notes, settings, and related workflow screens.
Fixed inventory notes, variance reasons, reorder vendor totals, dashboard recommendations, dirty-guard discard behavior, and physical-count variance units so saved changes and calculated totals stay current without manual reloads.
Release
5.3.9
Added
Added a 14-day Growth trial without requiring a credit card, with the existing subscription paused at trial end until payment details are added.
Changed
Streamlined signup to one operation-type question, then guides new owners through company setup inside Demi with a faster welcome-dashboard handoff.
Improved trial plan selection so choosing a post-trial plan preserves Growth access for the rest of the active trial.
Improved ingredient imports with clearer progress milestones, stronger vendor matching, and safer handling of packaging and supply rows.
Improved ingredient, recipe, inventory, and wastage screens so saved values and dashboard summaries refresh immediately without a browser reload.
Fixed
Fixed signup verification, reCAPTCHA error handling, workspace activation, and onboarding navigation so new users receive clearer errors and no longer get stuck on the final loader.
Fixed verified-library procurement access so vendor and cost records remain isolated to the active location.
Fixed Bulk Kitchen Production Lists so scheduled Bulk records take priority, actionability uses the correct schedule, and blank or zero shelf life produces same-day stock.
Fixed recipe duplicate validation so meals/products and sub-recipes can share a name while same-type duplicates remain blocked.
Fixed the GlobalAdmin onboarding dashboard so upload workspaces remain visible when administrators switch tenants or locations.
Fixed ingredient PAR edits, category updates, and inventory configuration versioning so Save/Undo state and table values remain consistent.
Security
Kept frontend source maps private for error diagnostics while preventing public source-map downloads.
Release
5.3.8
Added
Added a secure onboarding upload portal so superadmins can invite customers to submit recipe and ingredient setup files through private Demi storage.
Added customer-facing upload completion emails and admin review notifications for onboarding upload workspaces.
Changed
Improved onboarding upload workspaces with uploader resend/delete actions, duplicate email prevention, ZIP downloads, clearer public portal states, and Demi favicon coverage across public portal/API pages.
Improved vendor ingredient imports so pre-cut produce descriptions normalize more reliably.
Fixed
Fixed Kitchen Production List same-session navigation so returning to an already-loaded date or worklist keeps rows visible while backend validation runs silently.
Fixed scheduled Bulk Kitchen Production Lists so covered internal and customer orders are included even when the Bulk period also has configured Execution days.
Fixed Bulk workflow routing so demand loads through the correct workflow context.
Fixed the onboarding upload portal verification screen so it no longer shows empty placeholder controls and remains readable while secure links are checked.
Release
5.3.7
Added
Added a Wastage tracker announcement and guided tour for recording new wastage entries.
Added Kitchen Production List freshness manifests to support safer cache validation and future instant re-entry improvements.
Changed
Improved Inventory Wastage selection, dashboard refreshes, responsive layouts, modal behavior, export formatting, and PDF error handling.
Improved Kitchen Production List reliability for zero-demand scheduled days, internal-demand exception days, checklist polling, stale response handling, and cached lightweight payloads.
Improved bulk editor page-size handling so selected 50/100 row sizes render consistently across Products, Sub-recipes, and Ingredients.
Increased staging frontend build capacity and clarified Cloud Run deployment and Gemini review guidance for release operations.
Fixed
Fixed wastage item selection so tenant and location rows stay properly scoped and global/master rows do not leak into the selector.
Fixed wastage cost previews for recipes that use weight-based amounts of each-count ingredients so grams are converted through the ingredient's per-each weight instead of being counted as whole eaches.
Fixed Cost of Goods Sold reports so retail, wholesale, margin, and cached sales datasets scale normalized recipe costs to the full recipe yield.
Fixed recipe yield edits so blank locked portion fields no longer produce NaN Nutrition Facts displays.
Fixed recipe component dropdowns so inactive ingredients, sub-recipes, and products are not offered for new recipe/product composition while active and draft items remain selectable.
Fixed Orders API requests without filters so order lists and counts no longer return a 500 when where is omitted.
Release
5.3.6
Added
Added an Inventory Wastage tracker for ingredients, sub-recipes, and products, including estimated cost previews, stock movements, dashboard summaries, search, and PDF/Excel exports.
Changed
Improved Kitchen Production List and Bulk workflows so order saves, internal demand exceptions, date navigation, cached re-entry, and background validation refresh faster and stay visually stable.
Improved internal production management with fresher product lineup saves, clearer date selection, Today styling, product counts, header actions, filters, and search presentation.
Improved Kitchen Production List Print / Export actions with clearer blocked-action feedback and in-progress protection.
Fixed
Fixed Bulk Kitchen Production List false empty states, zero-demand rows, stale order payloads, and long-running generation progress handling.
Fixed Bulk exception days so demand-only exception records stay isolated from scheduled Bulk PAR/current-stock behavior while valid exception dates remain selectable.
Fixed recipe and sub-recipe optimistic create flows so temporary IDs no longer appear in headers, print titles, or persisted URLs after save.
Fixed wastage previews so component rows stay visible even when procurement or costing data is missing.
Release
5.3.5
Added
Added workflow filters and sortable workflow details to Sub-Recipe list and bulk-edit tables so operators can find Daily Prep, Combining, and Bulk sub-recipes more quickly.
Changed
Improved Kitchen Production List refresh behavior after internal-order saves, workflow changes, and Bulk exception days so newly generated demand appears more consistently on the first load.
Improved internal production dashboards and reports so internal demand is included in operational totals while commercial revenue and margin totals remain separate.
Improved inventory setup, inventory table actions, and Physical Counts navigation so pack-size labels, wizard columns, row clicks, and remove actions are clearer.
Improved production order loading states with Demi skeleton placeholders instead of plain loading rows.
Fixed
Fixed recipe and product component reorder saves so visual order persists after reload without unnecessarily updating the parent recipe timestamp.
Fixed Bulk exception demand for same-day internal production orders so needed quantities appear in the correct period and are not incorrectly offset by same-day production.
Fixed Kitchen Production List stale or blank states after saves, workflow changes, and background refreshes.
Release
5.3.4
Changed
Improved Kitchen Production List and internal production workflows so demand regeneration, date ranges, and production recalculation stay stable across refreshes and route changes.
Improved Bulk and late-order exception handling so exception demand days and internal order dates are represented more consistently.
Improved ingredient and recipe import review surfaces with clearer recipe recency badges.
Improved procurement costing displays so usable-amount purchase data is reflected more clearly.
Hardened customer struggle-signal capture and operator runbooks for follow-up workflows.
Hardened GCP Cloud Run deployment guidance for release and staging operations.
Updated HubSpot prospecting sender defaults to use the mail subdomain.
Fixed
Fixed internal production order saves by removing retry-only and unsafe payload fields before persistence.
Fixed stale Bulk production records so recovery paths safely refresh missing entities.
Fixed Typesense-backed table loading states so skeleton rows remain stable during refreshes.
Fixed container kit component picker dropdown rendering in modal flows.
Release
5.3.3
Added
Added secure signup resume links so interrupted trial signups can continue more reliably.
Changed
Improved recipe costing screens so they load with less blocking, keep rows stable during refreshes, and reflect procurement repairs more smoothly.
Improved nested recipe and sub-recipe costing updates so parent recipes and products stay current after costing fixes.
Improved sub-recipe yield setup, simple-pack costing, local unit/cost displays, and dashboard food-cost sales filtering.
Improved ingredient import progress, ETA accuracy, invoice costing details, and advisory review messages so vendor data is easier to verify.
Improved bulk editor saves so list updates stay smooth while changes are being confirmed.
Improved promo code and order discount flows.
Improved internal production planning, customer configuration checks, internal order KPL grouping, and Daily Prep production settings visibility.
Improved transactional email routing reliability.
Fixed
Fixed Growth-plan inventory access for eligible customers when subscription metadata is stale.
Fixed stale rows after saves so recently edited items appear with current data.
Fixed clipped modal action footers on affected screens.
Added backend onboarding conversion analytics so trial and setup funnel progress can be tracked more reliably.
Changed
Improved ROI calculator mobile conversion flow and Meta tracking compatibility so public conversion pages load and report more reliably.
Focused proactive AI Insights warm-up on eligible active/trialing AI Insights tenants and capped each location at 10 actionable pending insights instead of counting warmed recipes.
Improved proactive AI Insights candidate quality so empty product recipe shells are skipped before calling the AI service.
Improved importer reliability for vendor setup, vendor identity resolution, cancellation boundaries, invoice stream recovery, and supply import result messaging.
Improved Demi AI importer and label quality handling for deterministic Velocity spreadsheet parsing, durable matched-batch streaming, global supply-row classification, and cleaner generated label descriptions.
Updated founder trial follow-up cadence for the internal Sales OS workflow.
Fixed
Fixed container kit create/edit flows so component dropdowns populate merged packaging/supply items, optimistic creates save without temporary-ID read errors, and component selection uses clearer modal scrolling and toast copy.
Fixed workflow save and Bulk KPL date navigation behavior so refreshes and date arrows stay scoped to the selected workflow/date.
Fixed label generation so sub-recipe yield units are preserved for CFIA ordering.
Fixed Modern Catering meal plan reminders so duplicate reminders are no longer sent.
Suppressed internal recipe method notes from importer output.
Removed
Removed retired AWS deployment workflows from Demi app and Demi AI repositories.
Release
5.3.1
Changed
Improved ingredient and recipe import summaries so mixed food and supply imports report created, updated, supply, procurement, and skipped counts more clearly.
Improved supply and container kit workflows so packaging components are easier to select, track, and cost from imported vendor lists.
Fixed
Fixed fresh-workspace ingredient imports so shared matches are created in the active workspace instead of being counted as existing updates.
Fixed imported cleaning and operational supplies so they land in Supplies with the correct category/type instead of food ingredients.
Fixed count-based packaging procurement display and tracking setup so pack sizes, current stock setup, and cost-per-each values are easier to verify.
Aligned container kit component eligibility between the picker and backend saves so explicitly kit-eligible packaging supplies can be saved and rejected components are logged with diagnostic details.
Hardened HubSpot Sales OS prospect enrollment so missing company tokens and sequence user IDs are handled safely.
Improved Demi AI supply classification so labels, ingredient imports, recipe imports, and spec sheets route packaging and supply items consistently.
Release
5.3.0
Added
Modernized container kits as inventory-backed supplies, including kit composition, packaging COGS, and production depletion support.
Fixed
Fixed production deploy post-deploy markers so Typesense reindex/dashboard warm-up auth failures fail loudly instead of silently drifting.
Ensured supply/packaging category defaults and legacy packaging-to-supplies migration run during backend migrations.
Fixed supply add/edit saves so count-based usage units no longer require weight-per-unit nutrition data and immediate re-edits keep dirty-save guards active.
Release
5.2.6
Added
Added a sales-first Customer Users experience on customer records, including invite/edit flows for Customer Admin, Customer Location Admin, and the new order-only Buyer role.
Changed
Improved bulk editor save responsiveness and partial-failure recovery for ingredient, recipe, sub-recipe, and product batch edits.
Improved ingredient import procurement mapping so source price units drive LB/KG/case costing more reliably for imported vendor price lists.
Separated customer-user access from internal Team management and replaced the customer Users tab raw table with the shared paginated table and customer-scoped location picker pattern.
Extended user search-index metadata for customer/team access while keeping global user search hidden until secure user-assignment search is enabled.
Fixed
Hardened Stripe onboarding checkout sync, trial billing emails, promo-code checkout, and onboarding team invites so signup billing and default-location access complete more reliably.
Protected SaaS company owner roles in Team settings so Owner displays read-only, cannot be reassigned through generic team-member edits, and transfers keep owner/admin role permissions synchronized.
Fixed tenantless Super Admin startup, authentication bootstrap, inactive tenant switching, and platform-admin-only visibility for super_admin accounts.
Fixed ingredient name display and nutrition-facts serving-size unit saves so canonical ingredient names and manually typed gram servings persist correctly.
Hardened customer-user and customer-location-user listing/scoping so endpoints validate tenant/customer access and no longer hide authorization/data errors behind empty lists.
Security
Validated customer and location assignments before persisting user-tenant access, deriving subtenant access server-side instead of trusting client-provided subtenant IDs.
Release
5.2.5
Changed
Improved ingredient, sub-recipe, and product bulk editors so search and filters keep the complete editable row layout, preserve unsaved edits, and continue showing the newest fields after results reload.
Updated sub-recipe bulk workflow editing to use the current workflow options only: Daily Prep, Combining, and Bulk.
Improved product bulk editor pricing saves and cache invalidation so default retail and wholesale edits persist and refresh dependent product data reliably.
Fixed
Fixed production schedule setup so full-day cutoff times remain selectable and save correctly.
Fixed nutrition warnings so packaging supplies no longer show irrelevant recipe or ingredient nutrition alerts.
Fixed internal customer order creation so customers without delivery-location rows can be selected, validated, and saved as Internal orders.
Release
5.2.4
Added
Wired ROI calculator report requests into SendGrid Marketing and the Demi HubSpot Sales OS write path, including ROI lead tracking, native HubSpot Lead creation, and SendGrid lead-to-customer graduation on signup.
Changed
Polished the public ROI calculator page, report layout, hero typography, and enterprise-size location slider so operators with up to 100+ locations get clearer public reports.
Hardened the recipe importer V2 flow across streaming events, filename forwarding, source-unit preservation, missing-quantity review, computed sub-recipe yields, stale temporary matches, and AI extraction edge cases.
Fixed
Fixed GCP canonical-host tenant context so Super Admin company/location switches send the active tenant from switched JWTs, keep selected locations across reloads, and return tenant locations even when Global Admin middleware has not warmed the location cache.
Fixed GCP staging tenant-switch cookies so Super Admin company/location selection persists on canonical hosts without a cookie domain.
Fixed Modern Catering shop loading for canonical staging/prod hosts, subtenant-scoped lineups, logged-in sessions, and initial loader states while the area remains launch-gated.
Fixed a bug where Super Admins received a 401 Unauthorized error when switching to cancelled or inactive companies from the Companies section.
Fixed ingredient modal enrichment loading and pending-delete races during ingredient import cleanup.
Fixed TypeScript compilation and type compatibility errors in the tenant context middleware and the Square API service.
Security
Restricted Modern Catering areas and lineup data loading to logged-in Super Admins until Modern Catering is ready for broader customer access.
Release
5.2.3
Added
Prepared the Shopify integration foundation and Settings integration placeholder so tenant-scoped Shopify OAuth, mapping, webhook, price sync, and order-import work can continue behind a coming-soon entry point.
Added Gemini-backed label description generation and frontend synchronization so recipe and product labels can receive generated descriptions from the GCP AI service.
Changed
Improved ingredient import and reimport reliability by speeding review completion, prioritizing nutrition enrichment, stabilizing temp-reference ordering, refreshing procurement and label data, and preserving deleted-item tombstones.
Improved AI importer matching quality for structured names, coconut milk category handling, vendor filename inference, USDA fallback matching, subtenant-scoped procurement matches, and deleted tenant tombstones.
Removed trial urgency messaging from the in-app experience and polished ROI calculator mobile/report presentation.
Trimmed GitHub Actions usage across Demi app and AI services while keeping deploy and validation paths available.
Fixed
Fixed TypeSense and GCP post-deploy schema/reindex sequencing so batch mutations and newly sortable fields are read-after-write consistent after deployment.
Fixed Modern Catering unsubscribe links, stale shop sessions, internal production KPL recalculation, production selection scrolling, and inventory setup loading responsiveness.
Fixed bulk action toolbar usability during optimistic operations and success toasts.
Fixed the worklist tagging service Gemini deployment cleanup and migrated label tagging away from stale Gemini/OpenAI environment assumptions.
Release
5.2.2
Changed
Improved recipe and ingredient importer progress pacing so review flows advance more consistently, avoid misleading completion stalls, and refresh imported review data after background processing finishes.
Improved inventory setup performance by batching first-time configuration writes, opening-stock ledger saves, and search-index refreshes for larger ingredient selections.
Improved inventory setup wizard progress copy, table scrolling, row visibility, and unit dropdown usability across stock, PAR, and review steps.
Updated pricing plan rendering to use explicit Stripe plan metadata so only public recurring Starter and Growth plans appear beside the Enterprise contact-sales card.
Fixed
Fixed B2B order email unsubscribe handling so confirmations, cancellations, modifications, and reminders use the correct SendGrid transactional unsubscribe group.
Fixed inventory setup opening-stock saves by skipping blank or zero rows, validating unit conversions only when positive stock is entered, and targeting the deployed stock ledger tables.
Fixed purchase reorder recommendations so incompatible weight-to-each conversions no longer fall back to one-to-one quantities and create extreme demand spikes.
Fixed recipe and product bulk editor table scrolling so wide editable columns remain accessible and pagination no longer overlaps rows.
Fixed Demi CLI recipe and ingredient imports so OAuth users can upload local files, import the newest files from a folder, wait for completion, and avoid missing API route errors.
Fixed Demi CLI product tag lookup for OAuth users by adding the internal product-by-tag route used by demi products by-tag.
Release
5.2.1
Fixed
Fixed Kitchen Production List checkboxes so completed and unchecked states stay stable during live refreshes, polling, and cache invalidation.
Fixed stale notification mark-read updates so notification lists refresh reliably after items are read.
Fixed ingredient import review flows so procurement displays, ingredient list refreshes, nutrition enrichment timing, pack-unit normalization, and progress percentages are clearer and more reliable.
Fixed billing checkout confirmation payloads by removing prefilled customer email defaults and return URLs that could interfere with checkout completion.
Fixed AI Insights warm-up and swap recommendations so active product catalogs receive insights and ingredient swaps require stronger business evidence.
Fixed inventory setup wizard availability copy so setup requirements are easier to understand.
Fixed bulk-delete list refreshes and stale retry handling so deleted rows disappear reliably without duplicate retry errors.
Release
5.2.0
Added
Added the public Demi ROI calculator at /roi with operation-specific savings estimates, email-gated report details, and trial/demo calls to action.
Added the public Demi CLI landing page and OAuth-backed CLI resource flows for tenant-scoped orders, procurements, users, team members, settings, permissions, and locations.
Added the Square integration foundation and surfaced Square as a coming-soon option in Settings > Integrations.
Added regional and seasonal AI Insights, operator-grade opportunity types, and deterministic recommendations for supplier, procurement, prep consolidation, portion, menu-family, data-quality, and complexity improvements.
Changed
Improved AI Insights recommendations to prioritize recent active sales, measurable savings, regional seasonality, and actionable procurement cleanup before lower-value data cleanup opportunities.
Updated proactive AI Insights warm-up so Stripe subscription metadata and recent-sales availability no longer block active tenants with valid products from receiving insights.
Updated AI Insights cards to show verified savings labels and operational proof for non-cost opportunities instead of placeholder $0 saved messaging.
Hardened the AI services quality gates so seasonal and operator-grade insights tolerate edge-case market data, fractional savings, and validation anomalies without blocking useful recommendations.
Focused inventory setup eligibility on order, receive, and count readiness so recipe-costing gaps no longer block inventory setup.
Polished the Demi CLI OAuth login, browser callback screens, API documentation contrast, and app help-center links.
Fixed
Fixed tenant switching on the app host, AI Insights report navigation authorization, admin tenant header fallback, and recipe insight tenant context handling.
Fixed AI Insights dashboard review/apply actions, streaming operator merge behavior, payload sanitization, validation failure visibility, and seasonal refresh timing.
Fixed inventory setup partial saves, valid two-tier procurement rows, legacy zero case quantities, stale post-setup reloads, partial-success messaging, and final tour teardown/navigation focus.
Fixed global search category visibility, modal cache routing, team member refreshes, KPL lookup cache subtenant scoping, and route registration conflicts.
Fixed Demi CLI OAuth resource routing and order listing for tenant-scoped account sessions.
Security
Prevented public frontend source maps from exposing source content in deployed builds.
Release
5.1.0
Added
Added an in-app feedback widget in the top navigation that collects user context, attachments, and creates GitHub tracking issues with optional DevRev mirroring.
Added the AI Insights dashboard savings history view with accepted-savings KPIs, date range presets, accepted savings ledger, confidence scoring, refresh controls, and PDF/CSV exports for the current view.
Added Gemini webhook ingress for AI features.
Fixed
Fixed AI Insights accepted-change tracking so accepted insights are stored with exact recipe/insight metadata and no longer reappear in review lists.
Fixed proactive AI insight warm-up tenant loading by removing an invalid LoopBack include that caused recent cron runs to fail.
Fixed feedback submissions returning 403 on staging by ensuring the endpoint's authorization metadata is registered.
Fixed an issue where the cache monitor and other global admin endpoints returned a 403 Forbidden error for users with Global Admin privileges granted directly on their user record rather than via a tenant role.
Fixed an issue where uploaded images would not update in view-only mode due to browser caching by introducing a global cache invalidation strategy for recently uploaded images.
Fixed recipe/product image uploads so each upload gets a new storage object key and recipe image saves invalidate product lineup/customer ordering caches immediately.
Fixed feedback success toast message to display a friendly static text instead of raw GitHub issue numbers.
Bypassed stale recipe label caches to ensure up-to-date label data is retrieved.
Preserved label description generator URL across GCP deployments.
Guarded wizard PAR serialization in inventory setups.
Wired feedback GitHub token secret for deployment pipelines.
Restored global search entity results.
Hardened pricing sync path for Sysco integrations.
Persisted session dates and implemented in-flight load cancellation in Kitchen Production Lists (KPL).
Dropped redundant swaps between ingredients with identical names in AI Insights.
Prevented unit mismatch hallucination and improved mathematical cost scaling in AI Insights.
Implemented multi-level cost scaling and SSE stream enrichment architecture for AI Insights.
Preserved precise cursor position on ingredient list background updates in the frontend.
Resolved cursor focus drop and added keyboard navigation in the recipe builder.
Applied tenant versioning cloning to the V2 importer to prevent master data mutation.
Allowed Redis caching for tenant inventory queries to improve AI Insights performance.
Updated obsolete QBO sync links to point to the correct settings/integrations pages.
Handled spreadsheet import rejections and improved document mimetype validation.
Required QboStatus.ok for onboarding integration step completion.
Updated Help Center link in the welcome dashboard.
Release
5.0.5
Added
Added standard billing checkout interface to the onboarding flow, enabling smooth subscription payments for new users.
Fixed
Resolved Stripe v22 SDK typing issues and missing ui_mode properties to restore backend CI build stability and API checkout session creation.
Eliminated premature "invalid or expired link" UI flashes during the email verification and password reset flow.
Prevented checkout double-charges by restoring the correct billing interval toggles and fixing payment intent verifications for trial upgrades.
QBO invoice sync now resolves Location/DepartmentRef from the invoice's persisted sub-tenant and supports explicit QBO Department IDs per location, preventing Calgary/Vancouver invoices in the shared UpMeals QBO account from being classified under the wrong location after the GCP cutover.
Recipe conversion cache now immediately invalidates when modifications are made, ensuring optimistic saves map and render accurate yields in real-time.
Duplicating product lineups now properly copies customer-scoped QBO prefixes.
B2B users can now properly load and filter their own customers on the orders management page.
Excel export functionality for ingredients has been restored and stabilized.
Signup onboarding properly processes the checkout return state so new organizations flow into the app after successful payment.
Release
5.0.4
Added
Public API documentation now includes a generated changelog page, release archive data, and a latest-release preview on the developer portal home page.
Changed
The developer portal header, hero, and changelog presentation have been polished so API documentation and release notes are easier to scan.
Fixed
Nested sub-recipe costing now invalidates parent recipe/product costing cache immediately when components change, so newly-created product recipes no longer show stale $0.00 costs.
Valid signup reCAPTCHA tokens are no longer blocked when Google returns a valid token without a score, restoring affected account signup flows.
Release
5.0.3
Changed
Dashboard widgets now render without waiting for slower report calls, improving perceived load time on the home dashboard.
AI-powered features now route through the GCP Cloud Run AI services, restoring the production GCP AI service path after cutover.
Fixed
Google Maps, HubSpot forms, Google Tag Manager, and GA4 scripts now load under the GCP production content-security policy without console CSP violations.
Production static images now resolve from the GCP production asset bucket, including legacy filenames with spaces, commas, and percent signs.
The internal environment badge is hidden in production so end users no longer see staging/dev environment labeling.
Dashboard product links now route to the correct recipe-backed product records.
Reports tables now keep body columns aligned with headers and use synchronized widths across report views.
Meal plan reminders now send only to active participants.
Public backend documentation and asset pages such as /api-docs/, /architecture/, and /nfp_template.html are restored in the production Cloud Run backend image.
Subtenant-specific timezones are now respected in backend date calculations and order timezone displays.
Dashboard financial and product reports now exclude inactive recipe-backed products and inactive ingredients from sales, costing, and usage summaries.
Release
5.0.2
Changed
Post-v5.0.0 stabilization rollup. Captures the production and GCP staging hotfixes shipped after the Demi 5.0.0 release, including GCP cutover hardening, Cloud Run/SSE tuning, migration/deploy workflow fixes, and frontend CSP updates for the GCP-hosted app.
Fixed
Subtenant-specific timezones are now respected in backend date calculations and order timezone displays, so locations like Calgary no longer inherit the parent company timezone for production/cutoff timing.
Authentication, session recovery, and B2B access. Hardened invalid/expired token serialization, frontend 401/logout handling, LoopBack safe field validation, and restored Calgary/Vancouver B2B sub-tenant access after the GCP migration.
Kitchen production and product-lineup data integrity. Repaired legacy KPL tenant/date handling, duplicate/soft-deleted production-day recovery, product-lineup cache operations, and guarded product sales-association edits so lineup/customer associations are not accidentally deleted during product edits.
Dashboard, creation-flow, label, and report polish. Dashboard metrics now refresh on sub-tenant switches, transient inline warning badges are suppressed during item creation, SaaS label screens have their required language permission, label/QBO cache and uniqueness checks are correctly scoped, and reports/date-range layout issues are fixed.
GCP frontend connectivity. The nginx CSP for the GCP frontend now permits the production API and regional HubSpot script hosts while preserving existing Stripe, Google, PostHog, Typesense, Sentry, HubSpot, and Sales OS integrations.
GCP production analytics CSP. The GCP frontend CSP now permits Google Tag Manager and GA4 collection endpoints so production conversion tracking loads without console CSP violations.
GCP production static assets. The GCP production frontend now resolves static images from the production GCS asset bucket, safely encodes legacy image filenames with spaces/commas/percent signs, and permits HubSpot form counter images in CSP.
Added
Operational runbooks for the GCP migration window. Added Codex/GCP Cloud SQL access guidance plus Redis, product-lineup cache, and global label-content repair runbooks for faster post-cutover diagnostics and targeted recovery.
Release
5.0.1
Fixed
Calgary B2B sub-tenant access after cutover. Customer Admin and Customer Location Admin sessions can now load their assigned Calgary/Vancouver location context without hitting 403 on /subtenants/get-available. The endpoint now returns only the authenticated non-admin user's accessible locations, and backend sub-tenant authorization normalizes Mongo ObjectId values to strings before comparing IDs.
Reports table layout + date range selector timezone bugs. The reports table layout now aligns with the rest of the application. The date range selector component now properly handles timezone differences for same-year past dates, preventing incorrect width classes and formatting from being applied.
Release
5.0.0
Fixed
JWT Invalid Signature Handling (Backend 500s). Fixed a regression from the v5.0.0 deployment where users with old or manipulated localStorage JWTs encountered silent application failures instead of a clean logout. The backend user.repository.ts now explicitly catches JsonWebTokenError and wraps it in a custom TokenInvalidError (or TokenExpiredError), ensuring LoopBack 4 correctly serializes a 401 Unauthorized with a standardized code property. Before this fix, invalid signatures resulted in unhandled 500 Internal Server Errors.
Frontend Session State Traps. Re-architected the api.service.ts interceptor. The application now handles 401 HTTP errors unconditionally (regardless of the requested endpoint) when a token is present, instantly wiping IndexedDB caches, Entity stores, and localStorage to break the session lock. Replaced brittle string-matching error interpretation with structured JSON code parsing. Added a mutex (isLoggingOut) to prevent race conditions during concurrent API requests, protecting the router from multiple redirect attempts. All Angular interceptor behaviors are now covered by asynchronous Karma tests.
Fixed
JWT Invalid Signature Handling (Backend 500s). Fixed a regression from the v5.0.0 deployment where users with old or manipulated localStorage JWTs encountered silent application failures instead of a clean logout. The backend user.repository.ts now explicitly catches JsonWebTokenError and wraps it in a custom TokenInvalidError (or TokenExpiredError), ensuring LoopBack 4 correctly serializes a 401 Unauthorized with a standardized code property. Before this fix, invalid signatures resulted in unhandled 500 Internal Server Errors.
Frontend Session State Traps. Re-architected the api.service.ts interceptor. The application now handles 401 HTTP errors unconditionally (regardless of the requested endpoint) when a token is present, instantly wiping IndexedDB caches, Entity stores, and localStorage to break the session lock. Replaced brittle string-matching error interpretation with structured JSON code parsing. Added a mutex (isLoggingOut) to prevent race conditions during concurrent API requests, protecting the router from multiple redirect attempts. All Angular interceptor behaviors are now covered by asynchronous Karma tests.
Fixed
Modern Catering chrome now matches the approved mockup at /tmp/demi-mockups/modern-catering-revamp.html. PR #19 shipped the unified shell direction but drifted from the visual spec. Four corrections: (1) the "Catering" module pill moved from the topbar (where it was wedged between the sidebar toggle and the tenant switcher) to the sidebar logo bar — sits beside the Demi logo per mockup lines 988-998. (2) Pill recolor — soft badge-teal mint gradient swapped for the mockup's bright $teal-400 background + $teal-600 border + $demi-900 text so it pops against the dark green logo bar (mockup lines 220-225). (3) Removed the .modern-catering .btn .btn-primary SCSS override that forced every MC primary CTA (Add to cart, + Add New Meeting, Create Meal Plan, Save changes, etc.) to the legacy MC teal (#00B8A5) — buttons now inherit the standard Demi green (--demi-600 #089374) per mockup .d-btn-primary. (4) Sidebar mode-switching — when the user is on /mc/**, the sidebar now renders ONLY the Catering navigation group (Dashboard / Shop / Meetings / Meal Plans / Company / Locations / Team Members / Payment) preceded by a "← Back to Demi" return link. The full Demi admin nav (Dashboard / Companies / Sales / Recipes / Workflows / Inventory / Vendors / Reports / Settings / Admin) no longer renders alongside Catering — the previous implementation appended Catering to the bottom of the full Demi sidebar even for B2B users with only hasMCLocations. New CateringNavModeService.isMcRouteSnapshot synchronous getter lets SidebarComponent.rebuildLinks() branch on portal mode without subscribing inside the builder. Files touched: frontend/src/assets/scss/modern-catering/_variables.scss, frontend/src/app/layouts/{topbar,default-layout}/..., frontend/src/app/layouts/sidebar/sidebar.component.{ts,html}, frontend/src/assets/scss/layout/_sidebar.scss, frontend/src/app/modern-catering/catering-nav-mode.service.ts.
Changed
Modern Catering now renders inside the main Demi shell. The /mc/** routes resolve to DefaultLayoutComponent with a new CateringNavModeService (multicast isMcRoute$ / shopPageActive$ / hideShell$ / moduleLabel$ via shareReplay) driving body-class application, a teal "Catering" pill in the topbar, a gated Catering sidebar group (shown when CustomerACLService.hasMCLocations), and per-route profile links. /mc/join opts out of the shell via data.hideShell=true.
MC visual system aligned to Demi v5 tokens. Legacy hex (#00b8a5, #AE332C, #EC8F34, #4a4a4a, #E4E7EF, etc.) replaced with semantic tokens ($teal-600, $error-700, $warning-700, $text-primary, $gray-200) across dashboard, meetings, meal plans, checkout, shop, cart, and user profile. btn-purple/btn-orange/btn-outline-teal → btn-primary/btn-secondary. Custom .rounded-badge badge-* → badge badge-pill badge-{variant} badge-sm.
Stepper + wizard polish. Hand-rolled .circle-number and .step-bars markup in checkout + edit-meeting + edit-meal-plan replaced with <um-step-indicator>; active step now exposes aria-current="step".
Onboarding re-home. Removed frosted-glass background + !important shell-hiding rules; /mc/join now renders a centered card with the Demi green logo and Catering pill.
Cart body-class handling moved to CartStateService (Renderer2 + DOCUMENT), replacing direct document.body.classList access for SSR safety.
Removed
Parallel MC layout shell.McLayoutComponent, McTopBarComponent, and MC's own SidebarComponent deleted — their responsibilities now live in the shared DefaultLayoutComponent via CateringNavModeService.
Fixed
Status badges (Active / Draft / Inactive / Pending) — text now centers properly inside the pill. The shared getStatusBadgeHtml helper (frontend/src/app/services/helpers/status.helpers.ts) was emitting class="badge badge-pill … width-5", but the global .width-5 utility from frontend/src/assets/scss/layout/_dimensions.scss is unscoped — it applies width: 5rem !important to any element, not just td/th. Combined with the v5.0 premium-table redesign that switched .badge-pill to display: inline-flex; align-items: center, the dot+text content sat at flex-start inside an 80px box and left visible empty space on the right. Most noticeable on short labels ("Draft", "Active") in recipes, ingredients, and users tables; longer labels ("Inactive") hid the gap. Removed the redundant width-5 class so pills now auto-size to content; column width remains controlled by the existing col-width-7 text-center on the cell, so column layout is unchanged.
Release
5.0.0
Added
PO exports — status pill, receiving meta, per-line Status column, subtle Payment terms. Purchase Order PDF + Excel exports now reflect the receiving lifecycle. Header renders a coloured status pill (Draft / Submitted / Partial / Received / Cancelled) alongside the PO number plus a full Issued Apr 16, 2026 · 10:37 PM timestamp derived from submittedAt || createdOn. Meta grid is adaptive: Draft/Submitted shows cols-2 (Order date · Expected delivery), Partial/Received shows cols-4 (adds Received on · Received by sourced from the latest confirmed GoodsReceipt). Per-line Status column renders only for Partial/Received POs, classifying each line as Received / Partial / Pending by aggregating GoodsReceiptLineItem.receivedQuantity across all confirmed receipts for that PO and comparing against orderQuantity. Payment terms moved out of the heavy meta box into a subtle right-aligned Payment terms: Net 15 line above the totals block so AP finds it near the money context without a full-width bar dominating the header. Vendor account number now renders as Acct #… inline in the vendor party box (was a lonely meta cell). Shipping row added to totals, conditional on non-zero. Delivery instructions rendered as a full-width callout below the meta grid (was folded into the side notes block alongside totals). Excel layout mirrors the PDF — dynamic column count (#/Item/Pack/Qty/UoM/Unit Cost/Total + optional Status), pill-style cell fills on line Status via new received/partial/pending tokens in STATUS_COLORS, subtle Payment terms + Vendor account lines above the totals. Backend changes: PurchaseOrderExportService.loadExportData() now queries GoodsReceiptRepository.find({status: 'confirmed'}) + GoodsReceiptLineItemRepository.find({goodsReceiptId: in receiptIds}) in parallel with the existing PO/line-item/vendor fetches; POExportData interface extends with statusClass, issuedAt, receivedOn, receivedBy, and lineItems[].lineStatus. partially_received DB enum collapses to partial pill token. PDF template rewrites purchase-order.hbs meta section to use {{metaColClass}} + {{#if showReceivingMeta}} (both computed in generate-pdf.ts's computeMetaCols() — Partial/Received → cols-4, else → cols-2); new titlecase Handlebars helper supports the per-line pill labels. 5 new unit tests in purchase-order-export.service.test.ts cover status→pill-token mapping, receivedOn/By selection from the latest confirmedAt, per-line status aggregation across multiple receipts for the same line (Received / Partial / Pending all verified), and the status: 'confirmed' filter that keeps draft receipts out of the receiving meta. 33/33 export-service specs green.
HubSpot Sales OS — per-prospect Sequence routing (sequence_id input on send_day_0_and_enroll). Lets the /prospect skill classify each prospect (single-unit operator / multi-unit restaurant / other food-ops like catering, CPG, ghost kitchen, hotel F&B, meal prep) and enroll them in the matching HubSpot Sequence so the Day-0 template voice fits the segment. The lib already exposed a sequenceId arg on buildEnrollmentPayload; this PR plumbs it through the handler by reading parsed.inputFields.sequence_id, validating it's a numeric HubSpot sequenceId (regex ^\d+$, returns 400 send_day_0_and_enroll_invalid_sequence_id with echoed value on malformed input — guards against a typo deploying a stray prospect into an unintended cadence), and passing it into the enrollment POST. Omitted → falls back to FOUNDER_CADENCE_SEQUENCE_ID (531666928, the Multi-Unit Restaurant default) so existing callers keep working. sequenceId now logged on every send_day_0_sequence_enrolled line so Cloud Logging queries can slice enrollment volume by segment. Staging portal now has 3 Sequences live: Demi Founder Cadence (531666928, Multi-Unit Restaurant — Day 0 template 255143923), Demi Other Food Ops Cadence (532112375, Day 0 template 255144919), Demi Single Unit (clone) (532112374, Day 0 template 255144916). Day 4 / Day 9 / Day 16 / LinkedIn-Connect steps are shared templates across all 3 sequences (single source of truth — edit once, propagates) per HubSpot's "use existing templates" clone option. All 3 Day-0 templates follow the audit-approved standardized opener pattern from docs/drafts/sequence-mockup.html ("My name is Drew and I'm a chef, former foodservice operator, and co-founder of Demi." + {{ contact.demi_personalization_paragraph }} + segment-specific value-prop clause + "Do you have 15 mins to connect in the next week or so?" + "Best, Drew"). Because the per-contact personalization paragraph is the only variable block, agent classification cost is zero-token at send time (pick sequenceId, don't re-draft body). 97/97 control-plane specs green under node:test (lib already tested the buildEnrollmentPayload override path in the #1269 spec file; handler validation is additive). Follow-up: /prospect skill at ~/.claude/commands/prospect.md needs a classification block + mapping table (single_unit | multi_unit_restaurant | other_food_ops → sequenceId) plus an amended Phase 3 payload example — updated out-of-repo in the same session.
HubSpot Sales OS — Day-0 1-click Sequence enrollment. Replaces the Phase 1.5 Task-queue copy-paste flow with direct HubSpot Sequence enrollment on the native Sales Hub Pro path. /prospect now writes only a 1–2 sentence personalization paragraph to a new demi_personalization_paragraph Contact property, then enrolls the contact in Sequence 531666928 (Demi Founder Cadence) via the now-verified POST /automation/v4/sequences/enrollments endpoint (our private-app PAT was probed live — 400 validation errors on bad IDs confirm auth is open). Sequence Step 1 is configured in HubSpot UI as an "Email task" step referencing a "Demi Day 0" Sales Template whose body uses {{ contact.demi_personalization_paragraph | striptags | linebreaksbr }} + {{ contact.firstname }} — HubSpot resolves these tokens at composer-open time, so Drew's native Tasks queue entry opens a pre-filled composer he reviews + Sends in 1 click. Full HubSpot open / click / reply tracking applies; Sequence auto-advances to Day 4 after the send event. New pure lib/sequence-enrollment.mjs module exports buildEnrollmentPayload, isPersonalizationAcceptable, classifyEnrollmentError, ACTION_STATUS, and the constants — server.ts and the spec both import from it so tests regression-guard the real code (addresses PR review finding on mirrored logic). Hardening folded in from Gemini 3.1 Pro CTO review (accepted): ≥50-char personalization guard before enrollment (else 400 day_0_personalization_paragraph_too_short with actual char count, flagged for manual review); idempotency via demi_sequence_enrollment_id — pre-flight GET short-circuits duplicate enrollment; tightened error classification regex /already\s*enrolled|duplicate\s*enrollment/i so generic 409 Conflict responses (rate-limiter, association conflicts, concurrent writes) no longer misroute to the already-enrolled branch; partial-failure recovery — if POST /enrollments throws "already enrolled" but the contact property is empty (PATCH after enrollment failed previously), the handler now queries GET /automation/v4/sequences/enrollments?contactId=… to recover the ID from HubSpot directly and backfills the contact property, preventing stuck-in-retry loops; workflowActionCache now set on every success path (fresh enroll + idempotent short-circuit + race recovery) so re-fires of the same idempotency key skip the HubSpot GET; Lead stage transitions to inSequence (not approvedForOutreach) on successful enrollment — a successful POST /automation/v4/sequences/enrollments means the contact is by definition IN the sequence, and Cockpit funnel (Phase 3 in #1267) reads native Lead stages directly, so approvedForOutreach would have stranded every enrolled contact at the wrong bucket post-deploy. The same fire-and-forget transitionLeadForContact(inSequence) call now also runs on both idempotent short-circuit paths (pre-flight demi_sequence_enrollment_id hit + race-recovery remote-query hit), so Leads self-heal if a prior run enrolled in HubSpot but crashed before the Lead-side PATCH fired; ACTION_STATUS enum is Object.freezed so runtime mutation throws in strict mode. Rejected per CLAUDE.md "Gemini lacks repo context" protocol: Pub/Sub decoupling (overengineering for solo-founder 5-enrollments-per-batch use case, existing hubspotApi has 4-attempt 429/5xx backoff); BaseMultiTenantRepository (internal single-operator tooling, portal 342549762 is Demi's own — same rejection logic applied earlier tonight to Founder Cockpit); strict .ts for lib/ modules (repo pattern is .mjs + JSDoc: log.mjs, rate-limit.mjs, signature.mjs, prospect-funnel-metrics.mjs, convert-lead-to-deal.mjs); runtime feature-flag system (env-var pattern on Cloud Run is established, 60s flip). Drops Task HTML rendering block + demi_outreach_stage + demi_outreach_start_date writes from send_day_0_and_enroll (Sequence Step 1 replaces; legacy fields already retired in Phase 5 scope). New demi_personalization_paragraph Contact property added to scripts/hubspot-sales-os/lib/config.mjs so bootstrap.mjs --apply provisions it on any portal. 11 new specs in sequence-enrollment.test.mjs now import from the lib (not mirror the server) covering payload shape, personalization guard (6 edge cases), tightened error classification (guards against the over-permissive /conflict/ regex regression), and the frozen ACTION_STATUS enum contract with the /prospect skill. Drew one-time HubSpot UI setup required before deploying: create the contact property (or run bootstrap), create the "Demi Day 0" Sales Template with the HubL body, add Email task step as Day 0 / step 1 of Sequence 531666928. 97/97 control-plane specs green under node:test.
Founder Cockpit — Review Queue tasks now deep-link to the HubSpot task record. Each row in the Founder Review Queue widget renders as an <a target="_blank"> pointing at https://app-na3.hubspot.com/contacts/342549762/record/0-27/<taskId> (HubSpot Task objectTypeId 0-27). taskUrl is built in founder-cockpit-data.service.ts by stripping non-digits from task.id; when the stripped id is empty (legacy/malformed payloads) taskUrl is left undefined and the template falls back to a plain <div> row so the row still renders but isn't a broken /record/0-27/ link. Hover underline styling lives on a.task-link in .scss — does not apply to the skeleton or empty-state <div> rows. Two new specs lock the numeric-id → URL happy path and the no-digits → undefined fallback.
HubSpot Sales Pro migration — Phases 1.5 / 2 / 3 / 4. Completes the shadow-pipeline-to-native-primitives migration that started in #1263 (Phase 1). Phase 1.5 (Task-queue Day 0 flow):send_day_0_and_enroll control-plane action no longer creates a DRAFT email engagement (HubSpot's Engagements API is logging-only per the official spec — hs_email_status=DRAFT renders as a logged entry with no Send button in the UI, leaving drafts stranded; /sales-templates/v1/templates rejects private-app PAT auth at the endpoint level). The action now creates a HubSpot Task on the contact with the rendered Day-0 email body formatted as HTML + a deep-link to the contact record so Drew can open the native composer sidebar in one click, paste, review, and send natively. Task associates to both Contact (typeId 204) and Lead (typeId 647, verified live) so it surfaces on both timelines. New scripts/hubspot-sales-os/backfill-drafts-to-tasks.mjs (dry-run default) converts any existing stranded DRAFT engagements into equivalent Tasks + archives the originals via DELETE /crm/v3/objects/emails/{id} (recoverable via /restore) — dry-run against portal 342549762 confirmed all 5 pending drafts would convert cleanly with Contact + Lead associations intact. Phase 3 (Cockpit Lead-stage pivot):lib/prospect-funnel-metrics.mjs rewritten to aggregate Lead records from the native Demi Founder-Led Lead Pipeline (1739003844) instead of Contacts with custom demi_outreach_stage. New fetchAllProspectLeads() helper in server.ts mirrors the existing pagination cap pattern (MAX_COCKPIT_PAGES=5 → 500 Lead ceiling with cockpit_prospect_funnel_metrics_are_incomplete log-and-alert on overflow). The PROSPECT_STAGE_ORDER on the frontend data service now renders the 9 native Lead stages (New / AI Researching / Ready for Review / Approved for Outreach / In Sequence / Connected) and each bar deep-links to app-na3.hubspot.com/leads/342549762/pipeline/1739003844/stage/<stageId> — the native Kanban board replaces the provisioned saved-list deep links. Legacy ALL_PROSPECT_STAGES + CADENCE_OFFSETS remain exported for the Contact-based stage-SLA and stalled-nurture scanners that Phase 2 is retiring but still run on Contact state until Drew completes the HubSpot UI rewire. provision-day-0-drafts-list.mjs deleted — the native pipeline board supersedes saved lists 77/78/79. Server-side cache at HUBSPOT_COCKPIT_CACHE_TTL_MS (default 5 min) already in place — no cache code changes needed. Phase 4 (native Lead-to-Deal conversion on positive reply): new lib/convert-lead-to-deal.mjs extracted module drives the 4-call conversion flow that replaces /prospect Phase 5b's hand-rolled Deal creation: (1) GET /crm/v4/objects/leads/{id}/associations/deals short-circuits with convert_already_run if a Deal is already linked (handles double-convert race Gemini flagged), (2) POST /crm/v3/objects/deals creates the Deal on pipeline 1739003845 stage 2891461572 ("Discovery / Warm") with Contact (typeId 3) + Company (typeId 5) primary associations embedded atomically, (3) PUT /crm/v4/objects/leads/{leadId}/associations/deals/{dealId}/582 attaches the Primary-label Lead→Deal association, (4) PATCH /crm/v3/objects/leads/{id} transitions Lead stage to Qualified (2891461562). A correlationId is threaded through every log line + the DLQ entry so Cloud Logging can reconstruct the full flow. If step 4 fails after steps 2–3 succeed, a Firestore DLQ entry (kind: hubspot_convert_failure via the existing pushDeadLetter() path) captures {correlationId, leadId, dealId, contactId, companyId, reason, recoveryAction, error, ts} with a one-line recovery PATCH operators can replay — we do NOT compensate-delete the Deal (losing Deal history is worse than a Lead stuck pre-Qualified). New /internal/actions/convert-lead-to-deal Bearer-gated route exposes the lib to the /prospect skill. Phase 2 (send-detection scanner retirement + workflow rewire runbook):/internal/scans/day-0-send-detection returns 410 Gone unless ENABLE_DAY_0_SEND_DETECTION_SCANNER=true (rollback flag). The source + in-flight lock remain in-place for a 14-day rollback window per Gemini CTO review. Cloud Scheduler entry hubspot-day-0-send-detection removed from setup-cloud-scheduler.mjs; drew runs gcloud scheduler jobs delete per the new runbook tools/hubspot-sales-os/docs/phase-2-workflow-rewire.md, which also covers the HubSpot UI workflow rewire steps (trigger change on 3990922202 from Contact demi_outreach_stage=day_0 to Lead stage "In Sequence"; optional swap of Enrollem for native Enroll-in-Sequence; new native workflow on "Lead email sent by owner" event to flip stage). Gemini 3.1 Pro CTO review of the full cross-phase plan produced 2 CRITICAL, 3 HIGH, 2 MEDIUM, 1 LOW findings: the 3 accepted (Task HTML deep-link, convert-flow correlationId, 5-min TTL cache) are folded in; the 3 rejected (pivot Cockpit to multi-tenant scoping, full webhook-driven Firestore event-sourcing for metrics, SCSS token refactor) were documented as wrong-context false positives or explicit scope creep per the CLAUDE.md "Gemini lacks full repo context, verify against codebase" protocol. 12 new convert-lead-to-deal specs + 11 new backfill-drafts-to-tasks specs + 16 rewritten prospect-funnel-metrics specs (for Lead-based input) all green under node:test.
HubSpot Sales Pro migration — Phase 1 (additive dual-write to native Leads + Target Accounts). Portal 342549762 has Sales Hub Pro with the full native model already provisioned (Leads object 0-136, pipeline 1739003844 with 9 stages, Target Accounts, Sequences). Until this PR /prospect was writing to Contacts + custom demi_outreach_stage instead — a shadow pipeline that left Drew's native Leads / Target Accounts tabs empty. Phase 1 is purely additive: the Contact flow Drew uses today keeps running; new work ALSO creates a Lead in the native pipeline + flags Tier 1 Companies as Target Accounts. Back-fill is idempotent and one-shot: scripts/hubspot-sales-os/backfill-contacts-to-leads.mjs --apply walked the 27 existing Contacts with demi_outreach_stage set and created 27 Leads + flagged 15 Tier 1 Companies on live portal 342549762. Control plane send_day_0_and_enroll and the day-0 send-detection scanner now both call a new transitionLeadForContact() helper that patches the associated Lead's stage alongside the Contact-side update (Contact-side runs first and Lead-side is non-fatal, so Drew's existing flow can't break); every log line carries demi_migration_phase_1_active=true so the dual-write burn rate is queryable in Cloud Logging. prospect-funnel-metrics.mjs was extracted from server.ts into a pure-function module with 14 unit tests covering stage counting, rate math, DST-safe due-today arithmetic, and the documented "totalActive excludes terminal stages" contract. Extraction closed a latent bug where stageless contacts could inflate totalActive (defense-in-depth — the outer search filter prevents it reaching the aggregator in practice). New dedup path via GET /crm/v4/objects/contacts/{id}/associations/leads stops the back-fill from creating duplicates on re-run. /prospect skill Phase 1 Research adds two steps: create Lead with primary associations + set hs_is_target_account=true on Tier 1 Companies. Phases 2–5 (scanner retirement, cockpit pivot to Lead pipeline stages, native Lead-to-Deal conversion, property archival) ship in follow-up PRs after a 3–7 day bake. Reviewed in /Users/upmealsos/.claude/plans/shimmying-percolating-boot.md — CEO review (1 critical + 7 other findings, all resolved in-line) + eng review (2 critical + 9 other findings, 4 failure-mode gaps closed, state-machine ASCII diagram added, parallelization lanes plan). Runs on the existing HubSpotClient + node:test + node:assert/strict conventions; 56 passing specs total (+24 new).
Founder Cockpit — Prospect Funnel widget + saved HubSpot contact lists. New fourth row on /founder-cockpit renders every outreach stage (new / researched / day_0_drafted / day_0 / day_4 / day_9 / day_12 / day_16 / replied / demo_booked) with a horizontal bar, contact count, and deep link to the matching HubSpot saved list. Adjacent "Outreach KPIs" card surfaces total active, due today, tier 1 / tier 2 counts, replies, demos booked, reply rate, demo rate, and reply→demo conversion — all wired to the existing prospectFunnel payload from the control plane (frontend-only change; no backend edits). New scripts/hubspot-sales-os/provision-day-0-drafts-list.mjs idempotently provisions three DYNAMIC contact lists in the portal via the HubSpot Lists v3 API: "Day 0 Drafts — Ready to Send" (listId 77), "Sequence Enrolled — Day 4, 9, 16" (listId 78), "Replies — Needs Triage" (listId 79). Drew can now batch-review the 5 drafted cold emails from a single cockpit widget (click "Day 0 — Drafted" → HubSpot saved list) instead of clicking through contacts individually. Spec coverage added for the new funnel mapper, enrichment pct math, and the empty-funnel fallback.
HubSpot Sales OS — Day-0 send-detection scanner + day_0_drafted intermediate state. Closes the send-vs-draft timing gap in the Sequences-wins architecture. Previously send_day_0_and_enroll control-plane action set demi_outreach_stage=day_0 at DRAFT creation, which — when paired with the Enrollem enrollment workflow — scheduled the Sequence's Day-4 follow-up 3 business days from draft creation instead of 3 business days from actual send (so the Day-4 bump could fire before Drew had even sent the cold email). New flow: control plane sets day_0_drafted at DRAFT creation and pins the engagement ID on the contact via the new demi_outreach_draft_engagement_id property. New scanner POST /internal/scans/day-0-send-detection (added to tools/hubspot-sales-os-control-plane/server.ts) polls contacts in day_0_drafted (within a 7-day freshness window), reads the pinned draft engagement directly, and flips stage to day_0 once hs_email_status=SENT and hs_email_direction=EMAIL. Engagement-ID gating eliminates the false-positive on same-day historical outbound emails and removes the need for association pagination or timestamp heuristics. The stage flip triggers HubSpot workflow 3990922202 → Enrollem "Enroll Contact in Sequence" → Demi Founder Cadence. Cloud Scheduler job hubspot-day-0-send-detection (every 5 min, 07:00–19:59 Mon–Fri America/Vancouver) added to scripts/hubspot-sales-os/setup-cloud-scheduler.mjs — max ~5-min latency from Send click to sequence enrollment. Scanner hardening: in-flight lock short-circuits overlapping Cloud Scheduler runs with 429, bounded concurrency of 3 + per-contact try/catch isolates single-contact failures, and a 7-day demi_outreach_start_date filter prevents stuck-contact reprocessing. New day_0_drafted option added to the demi_outreach_stage HubSpot enum at displayOrder 3; new demi_outreach_draft_engagement_id string property added (via HubSpot Properties API). No migration of existing contacts needed.
Backend exports redesign — Phase C (PDF microservice templates + logo data-URI inlining) — rewrites all four Handlebars templates in services/demi-pdf-service/ (purchase-order.hbs, physical-count-report.hbs, physical-count-sheet.hbs, inventory-stock-list.hbs) to match the locked "After" mockup. Shared design tokens (brand #089374, text hierarchy, surface / border / pill palettes), tabular-nums on numeric columns, page-break discipline (tr, .kpi-row, .notes-block, .totals-block all get page-break-inside: avoid), thead { display: table-header-group } for multi-page header repeat, and @page { margin: 16mm 18mm; size: Letter }. Status pills, KPI cards, stacked item + SKU / category / vendor cells, totals block without the heavy 2px rule, notes block with gray surface (no yellow warning). Shared types.ts rewritten to match the new backend payload (currency, vendor.address as structured AddressLike, shipTo.attn, lineItems[].uomLabel, docId on PC data, locationName on Inventory tenant, statusTimeline dropped from PO). Routes pre-compute status-pill class, meta-column class, per-item subtitles, and variance classes server-side so the templates stay simple (no nested Handlebars ifs, no eq helper). The currency helper was renamed from currency to fmtMoney (Gemini CRITICAL #1) to avoid shadowing the currency data field — {{currency}} now safely resolves to the code; {{fmtMoney value currency}} formats money. data.items.map(...) and data.lineItems are now defensive against null / non-array (Gemini HIGH #2). The empty meta block now renders nothing at all (Gemini MEDIUM #5) via a server-computed hasMeta flag. The backend generatePdf() on the PO and Physical Count export services now converts tenant.logoUrl to a data: URI via the shared fetchLogoAsDataUri() helper before POSTing (Gemini MEDIUM #4) — Inventory already did this, but PO and PC were leaking raw HTTPS URLs that Puppeteer's setRequestInterception SSRF guard would abort, rendering as broken-image icons. 159 existing backend unit tests still green. Gemini 3.1 Pro pre-merge review: round 1 found 5 real issues (1 CRITICAL, 2 HIGH, 2 MEDIUM) all fixed; round 2 verdict GO.
Backend exports redesign — Phase B (frontend currency selector + receiving contact input) — PO edit modal now has a Currency dropdown (CAD / USD) below the Expected Delivery Date field; backend default still applies on create (tenant country → CAD/USD) but users can override per PO for cross-border purchases. Location detail page (/saas-management/settings/locations/:id) now has a Receiving contact for POs text input in the Location preferences section; the value populates the "Attn:" line on PO ship-to blocks when set, and is omitted entirely otherwise. Both fields persist through existing save paths — PurchaseOrder.currency is a column added in Phase A, receivingContactName lives inside SubTenantConfiguration (JSON). Frontend PurchaseOrder and SubTenantConfiguration TypeScript models extended to match.
Backend exports redesign — Phase A (shared helpers + Excel rewrites + field mapping) — rewrites the Excel generators for Purchase Orders, Physical Counts (Sheet + Report variants), and Ingredient Inventory to match the locked "After" mockup at /tmp/demi-mockups/exports-design-system.html. Excel outputs now get document-specific sheet names (no more Sheet1), frozen column header rows, dynamic AutoFilter ranges, real pageSetup with printTitlesRow, and status pills rendered as cell fills (not colored text). Two new shared helper modules: backend/src/services/helpers/export-format.helper.ts (formatDate / formatAddress / formatPhone / formatCurrency / getTenantHeader — address formatting canonicalizes country codes and rejects empty segments; phone strips the legacy CA- / US- prefix concatenation bug) and backend/src/services/helpers/excel-style.helper.ts (getStatusFill with ARGB palette, applyHeaderBlock, applyPrintSetup, setColumnHeader, freezeHeaderRow). fetchLogoAsDataUri extracted to logo-fetcher.helper.ts with an added fetchLogoAsBuffer variant for ExcelJS workbook.addImage embedding (HTTPS SSRF guard preserved). Field-mapping gaps closed: PurchaseOrder.currency field added (derived from tenant.configuration.address.country at create — US → USD, else CAD), SubTenantConfiguration.receivingContactName added (surfaces as "Attn:" on PO ship-to block when set, omitted otherwise), PO export service now loads vendor address via include: [{relation: 'address'}] and populates uomLabel on each line item via a batch UnitRepository.find({where: {id: {inq: [...]}}}) lookup, Ship To name now correctly assembled as ${tenant.name} — ${subTenant.name} (was previously dropping the tenant and showing only subtenant). Empty fields are now hidden entirely in the meta grid / KPI summary — no em-dash placeholders. Internal statusTimeline dropped from vendor-facing POExportData payload (was leaking audit trail onto the vendor doc). Gemini 3.1 Pro pre-merge review surfaced two real findings, both fixed: safeText regex now catches leading whitespace (/^\s*[=+\-@]/) so " =1+1" can't bypass the formula-injection guard; new logo-fetcher.helper.test.ts locks in 13 assertions on the SSRF guard + content-type → extension mapping. PO Excel drops the Shipping totals row (field isn't captured by the UI); Inventory drops the "Currency" meta cell (internal doc, not needed); Physical Count status pill moved to dedicated row 4 F4:G4 (was crowding the meta kickers in column 2). PDF generators still point at the external PDF microservice — PDF HTML template rewrites ship in a follow-up Phase C (separate repo); only Excel + payload shape ship in this PR. New backend/src/types/cacheable-lookup.d.ts is a minimal type stub for the untyped cacheable-lookup@6.1.0 dep introduced by #1237 (project tests run on compiled JS, so the missing declaration was silently masked until tsc --noEmit surfaced it during this work). 159 passing unit tests (146 baseline + 13 new logo-fetcher assertions).
Inventory pre-production hardening phase 3 — rounding at persistence boundaries + Typesense sync on depletion (#1243). New backend/src/utils/decimal-rounding.util.ts (roundForPersist, sumForPersist) applied at every ledger write (depletion, goods receipt, on-hand sum) so accumulated IEEE-754 drift can't reach DECIMAL(16,8) columns. MySQL was truncating unrounded floats producing UI-vs-ledger divergence on bulk receipts. Post-commit ingredientRepo.syncToTypeSense() now fires on depletion success (outside the tx try/catch — a sync failure can't rollback a committed ledger). 10 unit tests lock the drift-free contract.
Inventory pre-production hardening phase 4 — PO compare-and-swap locks + bulk-receive version fix + permission matrix (#1244). cancel(), bulkSubmit(), bulkCancel() on purchase orders now use updateAll({status: X}, {id, status: {inq: [...]}}) with bypassCache: true so concurrent status transitions fail with 409 Conflict (status race, not 400 BadRequest). bulkMarkAsReceived now captures the bumped version from updateReceiptItems before calling confirmReceipt — every multi-item bulk-receive previously 409'd on OCC because the stale receipt.version was passed through. Canonical permission matrix at docs/inventory-permission-matrix.md documents every inventory endpoint → @authorize mapping (audit result: clean across 5 controllers).
Inventory pre-production hardening phase 5 — frontend error handlers + wizard dirty guard + UI polish (#1245). stock-history.component.ts load failures now log, toast, and navigate back instead of silently stranding the operator on an empty view. receiving-list.component.ts and goods-receiving-route-wrapper.component.ts replace non-null-asserted onHidden! with safe onHidden?.pipe(...) so a rapid dismiss-before-subscribe can't blow up. Inventory-setup wizard close() prompts via AlertsService when dirty (selection changes or non-zero stock entered on new items); Number.isFinite guard in executeConfirmAndSave() bounces invalid stock back to step 2 with an actionable inline error instead of letting the backend reject with 400. Dashboard recommendations *ngFor gained trackBy: trackByVendorId to prevent DOM thrash when toggling a single vendor card. UI polish: dropped the colored left-border accent on .quick-action-banner, replaced the custom .table-empty div on PO-create with <um-empty-state>, swapped <i class="fa fa-redo"> for <fa-icon>.
Inventory pre-production hardening phase 6 — vendor XSS strip + field maxLength + E2E regression guards (#1246). New backend/src/utils/sanitize-vendor-strings.util.ts strips HTML tags from vendor free-text fields (name, mainContactName, notes, deliveryInstructions, accountNumber, paymentTerms, url, image). Two-pass (strip tags → decode </>/</< entities → strip again) closes the unterminated-tag and numeric-encoded-tag bypasses that a naive <[^>]*> regex misses. Applied at the top of VendorController create / updateAll / updateById / replaceById. maxLength constraints added to previously unbounded fields (name 255, mainContactName 255, url 500, image 500, notes 5000). E2E guards in tests/e2e/inventory/inventory-management.spec.ts cover the wizard dirty-close prompt (PR-16) and cross-tenant IDOR handling (phase 1).
Inventory reorder recommendations now open the modal Create PO — clicking "Start PO" from the dashboard's reorder-recommendations panel opens the modal create-PO experience with the vendor and suggested line items pre-populated (same path as + Create PO), instead of dumping the user into the legacy full-screen form.
Client-side inventory print — the dashboard's Print action renders a local print view (InventoryPrintService + PrintableInventoryComponent) instead of round-tripping a PDF from the external service. Opens the browser print dialog in under a second vs. the prior ~15-20s wait, matching the existing invoice-print pattern. Backend exposes ?format=json on /ingredient-inventory-config/export for the payload.
Changed
Premium table + global UI redesign (badges, dropdowns, headers, icons) (#1218). Global cosmetic overhaul elevating table UI to Linear/Notion quality. Tables: transparent header (Style B), elevated container shadow, lighter row dividers, brand-tinted hover with keyboard parity, bold name column with teal hover, refined table inputs. Badges: gradient backgrounds on all variants, status-dot indicator scoped to table cells only (td .badge-pill); dots suppressed on type/category/tag badges so they read as labels, not states. Dropdowns: elevated shadow, rounded hover items, destructive items in red with separator line, default far (regular) icon weight for menu items. Navigation: sidebar spacing + active-state treatment refined, group headings de-emphasized, nav icons unified to a single stroke weight across the entire app. Topbar: notification bell and help-center icons simplified to borderless style; notification badge redesigned (minimal red dot for counts <10, compact pill for 10+). Icons: registry consolidated (icons.registry.ts) so every page pulls from the same set — no more one-off SVG imports drifting out of sync. Applies repo-wide to every list page (companies, meetings, locations, meal plans, recipes, ingredients, containers, categories, team members) without touching their behavior.
SaleHub + PDF service GCP deploys build on the GitHub runner instead of Cloud Build. The four deploy-salehub-{staging,prod}.yml / deploy-pdf-service-{staging,prod}.yml workflows were rewritten to docker build + docker push + gcloud run deploy directly on the Actions runner, matching the pattern used by deploy-gcp-staging.yml. The two cloudbuild.yaml files (tools/hubspot-sales-os-control-plane/cloudbuild.yaml and services/demi-pdf-service/cloudbuild.yaml) are deleted. Motivation: gcloud builds submit was exiting with code 1 on every run because the deploy service account couldn't stream logs from the default Cloud Build logs bucket — the Cloud Build itself succeeded every time but CI showed red, masking real failures. Building on the runner removes Cloud Build from the deploy path entirely and restores an accurate exit code. The SaleHub staging workflow keeps the gcloud run services update-traffic --to-latest post-deploy step (shipped in #1270) as a first-class GitHub Actions step; prod workflows omit that step so the canary/pin strategy still governs prod traffic. PDF service staging + prod workflows now run npm install && npm run build on the runner before docker build (its Dockerfile expects a pre-built dist/). No service behavior changes — same images built, same Cloud Run flags (--memory=2Gi --cpu=2 --min-instances=1 --max-instances=10 --concurrency=4 --timeout=60s --no-allow-unauthenticated for PDF) — only the build location moves.
SaleHub control plane staging deploys now auto-promote to 100% traffic.tools/hubspot-sales-os-control-plane/cloudbuild.yaml gained a new post-deploy step that runs gcloud run services update-traffic hubspot-sales-os-control-plane --region=${_REGION} --to-latest when the new _PROMOTE_TO_LATEST substitution is "true". The staging workflow (deploy-salehub-staging.yml) passes _PROMOTE_TO_LATEST=true so every green build flips traffic onto its new revision; prod (deploy-salehub-prod.yml) leaves it empty so the existing pin/canary strategy (revision tags like hotfix, pr1258-merged, explicit manual update-traffic) still governs what serves production. The traffic flip is a separate update-traffic step (not a flag on gcloud run deploy) because --to-latest is documented on update-traffic, not on deploy.
Founder Cockpit — Revenue Forecast chart legend cleaned up; integration + feature docs refreshed to match shipped proxy architecture. The cockpit's forecast chart dataset was reduced to the probability-weighted series only in an earlier revision (upstream metrics endpoint returns a single totalAmount per month), but the HTML legend and subtitle still advertised a second "Best case" dashed line that was never rendered. Removed the orphan legend item and updated the subtitle to "Probability-weighted pipeline by close month" so the UI no longer lies about what's on the chart. Rewrote docs/integrations/hubspot-sales-os.md from the retired direct-to-control-plane architecture (GET /hubspot/cards/founder-cockpit/dashboard, no auth) to the shipped LoopBack-proxy reality (GET /api/cockpit/metrics → CockpitController → control plane with server-side Bearer token, session JWT + ManagePlatformSettings on the proxy, error-code taxonomy). Refreshed docs/features/founder-cockpit.md data-flow diagram (direct-call diagram → proxied flow with shareReplay(1)'d metrics$) and removed the stale STUB_DASHBOARD reference (service now returns emptyDashboard() on error, no mockup fallback ships).
ProductionDaysRecord.depletionStatus now reports 'partial' on Typesense-sync-only failures — the enum value existed but was never set. IngredientDepletionService.calculateAndRecordDepletion now returns a typesenseSyncFailed flag; production-data.service.ts sets depletionStatus = 'partial' when the ledger commits but one or more post-commit syncToTypeSense() calls reject. Ledger count is correct but search results may lag until sync is retried. Ops distinguishes this from 'failed' (ledger itself did not commit) via the dashboard badge.
Inventory PDF/Excel exports align with the dashboard's 5-band status logic — "Out of stock" (0 on hand), "Critical" (<50% PAR), "Below PAR", "In stock", "No PAR". Below-PAR summary count now includes out-of-stock items, matching the dashboard KPI. PAR column displays primary units only (secondary line removed). Backend export now treats parLevel === 0 as "In stock" (explicitly "no minimum needed"), matching the repository + frontend logic.
Expected Delivery Date is required when creating a PO manually — the Create PO modal and reorder Start PO flow now surface the * required indicator and the form blocks save until a date is chosen. Editing existing POs (including auto-generated drafts created without a date) is unaffected.
Received metadata split on received POs — the readonly PO view now shows "Received by {name}" and "Received on {full timestamp}" on separate rows to match the inventory print layout.
Optimistic bulk-cancel on the Purchase Orders list — bulk-cancelling draft/submitted POs flips each row's status badge to "Cancelled" immediately on API success, instead of waiting for a full reload.
Purchase Orders refresh button shows progress — the header refresh icon now spins and disables while a manual refresh is in flight, with a safety timeout that clears the spinner if the underlying list never emits.
Vendor → PO integration surfaced end-to-end (#1248) — selecting a vendor on the Create PO modal now auto-populates the Notes textarea from the vendor's "Default PO note" (vendor.deliveryInstructions). Preserves custom edits: notes only swap when the previous value still matches the prior vendor's default, tracked via _lastAppliedVendorNoteDefault. Readonly PO view now surfaces orderingEmail (as an "Orders" row), the vendor's structured street address, and renames "Submitted"/"Submitted by" → "Placed on"/"Placed by" (matching the existing Received on/Received by pattern, formatted as medium for consistency). The inline vendor caption below the Create-PO vendor select prefers orderingEmail over the rep email when both are set, since ordering email is the address POs will actually be sent to. Ingredient-name cell in line items dropped the ↑/↓ N% price-delta indicator — kept on the inventory dashboard, removed from PO context where it was noise.
Credit terms: removed Net 8 from the canonical CREDIT_TERMS constant — not a standard payment term.
Added
Vendor screen overhaul — rebuilt the Add/Edit Vendor modal on Demi's current settings-section layout (matches the edit-customer pattern). The modal now surfaces fields that already existed on the backend but were hidden in the UI, plus a new structured address:
Structured address via the global <um-address-input> (Google Places autocomplete) — persisted through a new addressId FK on Vendor and a shared Address row, mirroring the customer pattern. Vendor controller overrides create / updateById / replaceById to create-or-update the Address row and set addressId atomically. Migration migrate-vendor-address-and-index.ts adds a composite (tenantId, subTenantId, accountNumber) index and an addressId index online (ALGORITHM=INPLACE, LOCK=NONE).
Account #, Rep name (= mainContactName), Rep email (= email), Ordering email (tooltipped), Credit terms (ng-select backed by the new shared CREDIT_TERMS constant, with [addTag] so legacy DB values like Net 45 render and a trim+dedupe callback prevents duplicate tags), and Default PO note (= deliveryInstructions, tooltipped, maxlength="500") — all with aria-invalid bindings and markAsTouched on save so required-field errors light up on the first click.
Vendor list now includes an Account # column (sortable, backed by the new composite index).
Logo uploader switched from the legacy <um-upload-image> to the shared <um-image-uploader-row> component (matches customers / ingredients).
PO export DTO extended with orderingEmail, accountNumber, and vendor address so downstream PDF/XLSX templates can render them.
Ingredient importer populates vendor fields opportunistically — VendorInfo DTO extended with optional vendor_email, vendor_phone, vendor_account_number, vendor_payment_terms, vendor_ordering_email, vendor_postal_code, vendor_country_division, vendor_address2. When the upstream AI extractor supplies any of them, the importer stamps them onto newly-created vendors and best-effort fills empty columns on existing vendors (never overwrites user-populated fields). Email values are validated via the shared validateEmail helper; invalid values are dropped with a warning log. Extracted addresses now create a structured Address row instead of being stuffed into the free-text notes field.
VendorRepository write-path normalization — overrides create / createAll / updateById / updateAll / replaceById to collapse empty-string email / orderingEmail / accountNumber / phone / paymentTerms to null, so the PO send fallback (orderingEmail || email) can't land on "" and PATCH can explicitly clear a column.
Shared CREDIT_TERMS constant — mirrored in frontend/src/app/shared/constants/credit-terms.constant.ts and backend/src/constants/credit-terms.constant.ts, consumed by both edit-customer (refactored off its inline literal), the new edit-vendor, and the ingredient importer's normalizePaymentTerms helper so canonical values don't drift across the stack.
Vendor address lifecycle hardening — vendor controller uses delete-first-then-update semantics when a client clears the address (prevents orphans if the Vendor update fails); orphan-cleanup failures on create / clear / replace are escalated to Sentry with tags: { context: 'vendor-address', kind, vendorId, addressId } per the project Sentry convention. Address payload on create now requires address1 + city minimum (aligned with importer strictness) — partial country-only or postal-only payloads are rejected with HTTP 400 instead of polluting the Address table.
Ingredients bulk editor: explicit save/discard pattern — replaced auto-save-on-debounce with a Save/Discard banner (matching the recipes bulk editor UX). A dirty-state service (IngredientBulkEditorChangesService) tracks which rows have changed and emits a reactive count. Changes are batched and saved sequentially; partial failures leave failed rows dirty so users can fix and retry without losing context. Navigation away with unsaved changes triggers a confirmation dialog via the existing UnsavedChangesGuard.
Ingredients bulk editor: Par stock and Track inventory columns — IngredientInventoryConfig records (separate API model) are batch-fetched after each page load and merged into the row data. Par stock accepts a number input; Track inventory uses a toggle switch. Both fields participate in dirty tracking and are saved (or upserted if no config exists yet) alongside the ingredient on batch save. A skeleton/spinner is shown in those cells while configs are loading.
Ingredients bulk editor: 3-state status selector — replaced the Active/Inactive toggle with um-status-selector [includeDraft]="true" so Draft status is now selectable directly in the bulk editor.
Ingredients bulk editor: Label name moved to Procurement group — Label name column relocated from the Core attribute group to the rightmost position in the Procurement group, matching the edit-recipe form layout.
Search and filters in bulk mode (Ingredients) — when Typesense search is active, the Typesense table is kept alive via [hidden] (instead of destroyed via *ngIf) so its data feed continues in bulk mode. A dedicated filter strip (search input, Status, Ingredient Categories dropdowns, Reset button) is shown above the bulk editor. Filter controls lock (opacity + pointer-events: none) when unsaved changes are present to prevent silent data loss from a page re-fetch.
Search and filters in bulk mode (Sub-Recipes / Products) — the existing filter bar (Status, Recipe Categories, Workflows) is already rendered outside the list/bulk mode guard, so it remains visible and functional in bulk mode. Filter controls lock identically when unsaved changes are present.
Products (Meals) bulk editor: Default retail price and Default wholesale price columns — two number inputs (step 0.01, min 0) appear in a Pricing column group only when recipeTypeName === 'Meal'. Both fields are tracked for changes, restored on Discard, and included in the batch save payload.
Fixed
Exports polish v3 — PO payment-terms snapshot fallback + collapsed delivery block + Location meta de-duped on Inventory. Three fixes spanning backend export services and the PDF microservice templates:
**PO Payment Terms / Vendor Account fall back to the vendor when the PO snapshot is null *or* empty string.** po.paymentTerms and po.vendorAccountNumber are captured at create time; if the vendor's terms/account are filled in after the PO was drafted, the snapshot stays null and the export would render nothing. Export service now does po.paymentTerms || vendor.paymentTerms || undefined (same for account number) so a newly-configured vendor field flows through to in-flight POs. Empty-string sentinel (user cleared the field intentionally) also falls through to the live vendor value — matching the || null guard elsewhere in the service so the two fallback chains stay consistent. Four new unit tests lock the frozen-snapshot / fallback / empty-string / account-number cases.
PO Delivery Date collapsed into the Delivery notes block. The meta grid previously rendered a standalone "Delivery Date" cell as a full-width bar whenever only that field was set (Payment Terms + Vendor Account empty) — visually awkward. Delivery Date now lives inside the notes block as a small kicker + value row above the notes body, and the meta grid only renders Payment Terms / Vendor Account (2 cells or none). Applies to both the PDF template (purchase-order.hbs with a new .date-row style + hasSummaryNotes flag) and the Excel generator (DELIVERY block condensed with date + notes, meta row-layout recomputed to skip empty strips entirely).
Inventory stock list dropped the duplicate "Location" meta cell. Location is already rendered as the uppercase kicker under the tenant wordmark in the header, so repeating it as a dedicated meta cell was duplicative (and rendered as a lonely full-width bar when no filter was applied). Meta grid now renders only when filterLabel is set, otherwise no grid at all. Applies to inventory-stock-list.hbs and InventoryExportService Excel output.
Inventory ship audit — 8 pre-merge fixes from the 2026-04-20 code sweep (#1249, docs/reviews/inventory-module-ship-audit-2026-04-20.md):
Dashboard → Create PO route. Reorder "Create PO" CTA targeted /inventory/purchase-orders/add, which is not a real route in the inventory module. Now navigates to /inventory/purchase-orders/new; PurchaseOrderCreateComponent.checkForPreFillState() unwraps history.state.prefill (the list-modal shape) and the legacy flat shape, then clears all prefill keys from history so back/forward nav cannot re-seed the form. Adds a dashboard regression spec.
Physical count explicit-zero persistence. Frontend dropped the backend isExplicitlyCounted flag on load, so a user's explicit 0 count reopened as "uncounted" (blank input, hidden variance). IngredientPhysicalCountItem now declares the field; wasExplicitlyCounted() honors the persisted flag (=== true) and falls back to countedAmount > 0 for legacy rows saved before the flag existed. countedAmount === 0 → null coercion now applies only to untouched rows (both the normal and degraded load paths).
Physical count dirty guard.isDirty previously compared only shelf amount + notes; getChangedItems() also syncs isExplicitlyCounted — page-level dirty state drifted from the save payload and navigation guards could report "no unsaved changes" after an explicit-count action. Both now share a single isRowChanged() predicate that also diffs the explicit-count flag against loadedExplicitSnapshot. Degraded-load path now seeds the snapshots so isDirty doesn't fire on a fresh load.
Physical count KPI math.summaryCountedSoFar used countedAmount > 0, excluding explicit zeros; summaryAvgVariancePercent coerced undefined variance to 0 and averaged across untouched rows, diluting the KPI toward zero. Counted KPI now uses isExplicitlyCounted === true; variance average filters by isExplicitlyCounted && theoretical > 0 && variancePercent != null. Specs updated to lock in the new semantics + new regression tests for explicit-zero and cleared-count cases.
Physical count list empty state. Zero-results on an active filter rendered the first-time onboarding overlay (blurred table + "Start Your First Physical Count"). First-time overlay is now gated on !hasActiveFilters; a filtered-empty message + Reset CTA matches the receiving-list pattern. Table gets inert when the blur overlay is active for correct focus/AT semantics.
Stock adjustment numeric validation.type="text" input accepted arbitrary non-numeric characters and the save button disabled only on adjustmentNewLevel == null — the delta display could render NaN and the click would round-trip to a 422. New canSaveAdjustment getter requires a finite non-negative number; onAdjustmentLevelChange normalizes via Number.isFinite and clamps negatives; onAdjustmentKeydown filters to [0-9.] while allowing Ctrl/Cmd/Alt combos so paste/copy/select-all still work; onAdjustmentPaste splits on . (robust against multi-dot paste) and routes through onAdjustmentLevelChange instead of mutating input.value.
Receiving list row-click consistency. Every row declared [rowClickable]="true" but the handler read item.draftReceiptId on the paginated-table's {item, event} emit payload — every row click was a silent no-op. onRowClick now accepts both payload shapes, routes draft rows to the receiving modal, and navigates non-draft rows (upcoming, completed, overdue) to the PO detail with returnUrl=/inventory/receiving. formatExpectedDate gained an isFinite guard so a malformed ISO renders "-" instead of "Invalid Date" (CLAUDE.md API/Performance rule).
PO edit save button.[disabled] bound only to !selectedVendorId || isSaving, so on new POs the button looked ready while the form was invalid because the expected-delivery-date was empty. New canSaveOrder getter composes vendor + isSaving + editFormRef.form.invalid; stays disabled until @ViewChild('editForm') is populated so the first-render window can't bypass the form guard. Existing ngAfterViewInitcdr.detectChanges() already mitigates ExpressionChangedAfterItHasBeenCheckedError for the new ViewChild-dependent binding.
Hardened through code-simplifier, code-reviewer, Gemini 3.1 Pro (Vertex AI) CTO review, and a post-open GitHub review pass (claude-bot + Codex + Gemini Code Assist). Five rounds in total — two critical findings (degraded-load snapshot gap, canSaveOrder fallback default), two UX findings (modifier-key pass-through, DOM-mutation-in-paste), and one legacy-data finding (isExplicitlyCounted strict-null-check downgrading pre-migration positive counts) all addressed.
PO reorder + auto-generate now include Bulk KPL PAR uplift (#1250, docs/reviews/kpl-bulk-vs-orders-inventory-verification-2026-04-20.md). PurchaseOrderService.calculateUpcomingDemand() previously only queried ProductionDaysRecord with type: 'Execution' — the toMake amount on Bulk records (which represents bulk recipe production when a user sets parStock above actual order demand) was never read. Tenants who set an explicit Bulk PAR above actual orders saw their ingredient reorder recommendations under-order the difference, because the PAR-driven extra production wasn't visible to PO demand.
Unified KPL query now fetches type: {inq: ['Execution', 'Bulk']} in a single round-trip. Execution records feed kplRecipeAmounts as before; Bulk records feed a new bulkUpliftAmounts map containing only the PAR uplift — max(0, toMake - amountUsedInWindow) — so the order-driven portion (already captured by Execution / order expansion via bulk-as-sub-recipe traversal) is not counted twice.
Partial-window overlap handling. A Bulk KPL's periodStartDate..periodEndDate (often 7+ days) routinely extends beyond the reorder window (3–14 days). Subtracting the full amountUsed over-subtracts → under-orders the out-of-window portion. The fix sums the per-day amountUsedByExecutionDay breakdown over datesInWindow to get the exact in-window order contribution. Legacy records without the breakdown fall back to total amountUsed (documented tradeoff: mild under-order risk vs. missing data).
Reason-code gate.kplFound now counts Execution + Bulk records so a Bulk-only window (e.g., PAR already satisfied by on-hand, zero uplift) surfaces as NoTrackedIngredientDemand (yellow "data exists, no demand") rather than being mis-routed through the NoOrdersInWindow "no data" branch. The truly-empty case (zero orders, zero Execution, zero Bulk) still correctly hits NoOrdersInWindow. Logger reports kplFound (Execution), bulkFound, and bulkUpliftRecipes separately.
Yield batch-fetched once for the union of Execution + Bulk recipe IDs — no extra DB round-trip over the prior code path.
Date invariant documented. Bulk records carry a single date marking the day the kitchen actually produces (ingredients consumed in one shift, then served from stock across periodStartDate..periodEndDate). Filtering by date: {inq: datesInWindow} therefore correctly captures every production event whose ingredient demand falls in the window — past-dated bulks are already reflected in current on-hand via getOnHandBulk.
Nine unit tests lock in the contract: PAR uplift when PAR > orders; zero uplift when orders cover; combined Execution + Bulk expansion inputs; unified query shape; legacy records without amountUsed; orders + Bulk same-day double-count regression; partial-window overlap math; Bulk-only window → NoTrackedIngredientDemand; truly-empty window → NoOrdersInWindow (control).
Hardened through code-simplifier, code-reviewer, Gemini 3.1 Pro (Vertex AI) CTO, and a post-open GitHub review pass (claude-bot + Codex + Gemini Code Assist). Three substantive findings fixed (CRITICAL partial-window under-count, P2 kplFound Bulk-only misrouting, 🟠 date-invariant documentation); MEDIUM legacy-fallback tradeoff and LOW reason-code coupling acknowledged.
Added
Backend exports redesign — Phase A (shared helpers + Excel rewrites + field mapping) — rewrites the Excel generators for Purchase Orders, Physical Counts (Sheet + Report variants), and Ingredient Inventory to match the locked "After" mockup at /tmp/demi-mockups/exports-design-system.html. Excel outputs now get document-specific sheet names (no more Sheet1), frozen column header rows, dynamic AutoFilter ranges, real pageSetup with printTitlesRow, and status pills rendered as cell fills (not colored text). Two new shared helper modules: backend/src/services/helpers/export-format.helper.ts (formatDate / formatAddress / formatPhone / formatCurrency / getTenantHeader — address formatting canonicalizes country codes and rejects empty segments; phone strips the legacy CA- / US- prefix concatenation bug) and backend/src/services/helpers/excel-style.helper.ts (getStatusFill with ARGB palette, applyHeaderBlock, applyPrintSetup, setColumnHeader, freezeHeaderRow). fetchLogoAsDataUri extracted to logo-fetcher.helper.ts with an added fetchLogoAsBuffer variant for ExcelJS workbook.addImage embedding (HTTPS SSRF guard preserved). Field-mapping gaps closed: PurchaseOrder.currency field added (derived from tenant.configuration.address.country at create — US → USD, else CAD), SubTenantConfiguration.receivingContactName added (surfaces as "Attn:" on PO ship-to block when set, omitted otherwise), PO export service now loads vendor address via include: [{relation: 'address'}] and populates uomLabel on each line item via a batch UnitRepository.find({where: {id: {inq: [...]}}}) lookup, Ship To name now correctly assembled as ${tenant.name} — ${subTenant.name} (was previously dropping the tenant and showing only subtenant). Empty fields are now hidden entirely in the meta grid / KPI summary — no em-dash placeholders. Internal statusTimeline dropped from vendor-facing POExportData payload (was leaking audit trail onto the vendor doc). Gemini 3.1 Pro pre-merge review surfaced two real findings, both fixed: safeText regex now catches leading whitespace (/^\s*[=+\-@]/) so " =1+1" can't bypass the formula-injection guard; new logo-fetcher.helper.test.ts locks in 13 assertions on the SSRF guard + content-type → extension mapping. PO Excel drops the Shipping totals row (field isn't captured by the UI); Inventory drops the "Currency" meta cell (internal doc, not needed); Physical Count status pill moved to dedicated row 4 F4:G4 (was crowding the meta kickers in column 2). PDF generators still point at the external PDF microservice — PDF HTML template rewrites ship in a follow-up Phase C (separate repo); only Excel + payload shape ship in this PR. New backend/src/types/cacheable-lookup.d.ts is a minimal type stub for the untyped cacheable-lookup@6.1.0 dep introduced by #1237 (project tests run on compiled JS, so the missing declaration was silently masked until tsc --noEmit surfaced it during this work). 159 passing unit tests (146 baseline + 13 new logo-fetcher assertions).
Inventory pre-production hardening phase 3 — rounding at persistence boundaries + Typesense sync on depletion (#1243). New backend/src/utils/decimal-rounding.util.ts (roundForPersist, sumForPersist) applied at every ledger write (depletion, goods receipt, on-hand sum) so accumulated IEEE-754 drift can't reach DECIMAL(16,8) columns. MySQL was truncating unrounded floats producing UI-vs-ledger divergence on bulk receipts. Post-commit ingredientRepo.syncToTypeSense() now fires on depletion success (outside the tx try/catch — a sync failure can't rollback a committed ledger). 10 unit tests lock the drift-free contract.
Inventory pre-production hardening phase 4 — PO compare-and-swap locks + bulk-receive version fix + permission matrix (#1244). cancel(), bulkSubmit(), bulkCancel() on purchase orders now use updateAll({status: X}, {id, status: {inq: [...]}}) with bypassCache: true so concurrent status transitions fail with 409 Conflict (status race, not 400 BadRequest). bulkMarkAsReceived now captures the bumped version from updateReceiptItems before calling confirmReceipt — every multi-item bulk-receive previously 409'd on OCC because the stale receipt.version was passed through. Canonical permission matrix at docs/inventory-permission-matrix.md documents every inventory endpoint → @authorize mapping (audit result: clean across 5 controllers).
Inventory pre-production hardening phase 5 — frontend error handlers + wizard dirty guard + UI polish (#1245). stock-history.component.ts load failures now log, toast, and navigate back instead of silently stranding the operator on an empty view. receiving-list.component.ts and goods-receiving-route-wrapper.component.ts replace non-null-asserted onHidden! with safe onHidden?.pipe(...) so a rapid dismiss-before-subscribe can't blow up. Inventory-setup wizard close() prompts via AlertsService when dirty (selection changes or non-zero stock entered on new items); Number.isFinite guard in executeConfirmAndSave() bounces invalid stock back to step 2 with an actionable inline error instead of letting the backend reject with 400. Dashboard recommendations *ngFor gained trackBy: trackByVendorId to prevent DOM thrash when toggling a single vendor card. UI polish: dropped the colored left-border accent on .quick-action-banner, replaced the custom .table-empty div on PO-create with <um-empty-state>, swapped <i class="fa fa-redo"> for <fa-icon>.
Inventory pre-production hardening phase 6 — vendor XSS strip + field maxLength + E2E regression guards (#1246). New backend/src/utils/sanitize-vendor-strings.util.ts strips HTML tags from vendor free-text fields (name, mainContactName, notes, deliveryInstructions, accountNumber, paymentTerms, url, image). Two-pass (strip tags → decode </>/</< entities → strip again) closes the unterminated-tag and numeric-encoded-tag bypasses that a naive <[^>]*> regex misses. Applied at the top of VendorController create / updateAll / updateById / replaceById. maxLength constraints added to previously unbounded fields (name 255, mainContactName 255, url 500, image 500, notes 5000). E2E guards in tests/e2e/inventory/inventory-management.spec.ts cover the wizard dirty-close prompt (PR-16) and cross-tenant IDOR handling (phase 1).
Inventory reorder recommendations now open the modal Create PO — clicking "Start PO" from the dashboard's reorder-recommendations panel opens the modal create-PO experience with the vendor and suggested line items pre-populated (same path as + Create PO), instead of dumping the user into the legacy full-screen form.
Client-side inventory print — the dashboard's Print action renders a local print view (InventoryPrintService + PrintableInventoryComponent) instead of round-tripping a PDF from the external service. Opens the browser print dialog in under a second vs. the prior ~15-20s wait, matching the existing invoice-print pattern. Backend exposes ?format=json on /ingredient-inventory-config/export for the payload.
Changed
ProductionDaysRecord.depletionStatus now reports 'partial' on Typesense-sync-only failures — the enum value existed but was never set. IngredientDepletionService.calculateAndRecordDepletion now returns a typesenseSyncFailed flag; production-data.service.ts sets depletionStatus = 'partial' when the ledger commits but one or more post-commit syncToTypeSense() calls reject. Ledger count is correct but search results may lag until sync is retried. Ops distinguishes this from 'failed' (ledger itself did not commit) via the dashboard badge.
Inventory PDF/Excel exports align with the dashboard's 5-band status logic — "Out of stock" (0 on hand), "Critical" (<50% PAR), "Below PAR", "In stock", "No PAR". Below-PAR summary count now includes out-of-stock items, matching the dashboard KPI. PAR column displays primary units only (secondary line removed). Backend export now treats parLevel === 0 as "In stock" (explicitly "no minimum needed"), matching the repository + frontend logic.
Expected Delivery Date is required when creating a PO manually — the Create PO modal and reorder Start PO flow now surface the * required indicator and the form blocks save until a date is chosen. Editing existing POs (including auto-generated drafts created without a date) is unaffected.
Received metadata split on received POs — the readonly PO view now shows "Received by {name}" and "Received on {full timestamp}" on separate rows to match the inventory print layout.
Optimistic bulk-cancel on the Purchase Orders list — bulk-cancelling draft/submitted POs flips each row's status badge to "Cancelled" immediately on API success, instead of waiting for a full reload.
Purchase Orders refresh button shows progress — the header refresh icon now spins and disables while a manual refresh is in flight, with a safety timeout that clears the spinner if the underlying list never emits.
Vendor → PO integration surfaced end-to-end (#1248) — selecting a vendor on the Create PO modal now auto-populates the Notes textarea from the vendor's "Default PO note" (vendor.deliveryInstructions). Preserves custom edits: notes only swap when the previous value still matches the prior vendor's default, tracked via _lastAppliedVendorNoteDefault. Readonly PO view now surfaces orderingEmail (as an "Orders" row), the vendor's structured street address, and renames "Submitted"/"Submitted by" → "Placed on"/"Placed by" (matching the existing Received on/Received by pattern, formatted as medium for consistency). The inline vendor caption below the Create-PO vendor select prefers orderingEmail over the rep email when both are set, since ordering email is the address POs will actually be sent to. Ingredient-name cell in line items dropped the ↑/↓ N% price-delta indicator — kept on the inventory dashboard, removed from PO context where it was noise.
Credit terms: removed Net 8 from the canonical CREDIT_TERMS constant — not a standard payment term.
Added
Vendor screen overhaul — rebuilt the Add/Edit Vendor modal on Demi's current settings-section layout (matches the edit-customer pattern). The modal now surfaces fields that already existed on the backend but were hidden in the UI, plus a new structured address:
Structured address via the global <um-address-input> (Google Places autocomplete) — persisted through a new addressId FK on Vendor and a shared Address row, mirroring the customer pattern. Vendor controller overrides create / updateById / replaceById to create-or-update the Address row and set addressId atomically. Migration migrate-vendor-address-and-index.ts adds a composite (tenantId, subTenantId, accountNumber) index and an addressId index online (ALGORITHM=INPLACE, LOCK=NONE).
Account #, Rep name (= mainContactName), Rep email (= email), Ordering email (tooltipped), Credit terms (ng-select backed by the new shared CREDIT_TERMS constant, with [addTag] so legacy DB values like Net 45 render and a trim+dedupe callback prevents duplicate tags), and Default PO note (= deliveryInstructions, tooltipped, maxlength="500") — all with aria-invalid bindings and markAsTouched on save so required-field errors light up on the first click.
Vendor list now includes an Account # column (sortable, backed by the new composite index).
Logo uploader switched from the legacy <um-upload-image> to the shared <um-image-uploader-row> component (matches customers / ingredients).
PO export DTO extended with orderingEmail, accountNumber, and vendor address so downstream PDF/XLSX templates can render them.
Ingredient importer populates vendor fields opportunistically — VendorInfo DTO extended with optional vendor_email, vendor_phone, vendor_account_number, vendor_payment_terms, vendor_ordering_email, vendor_postal_code, vendor_country_division, vendor_address2. When the upstream AI extractor supplies any of them, the importer stamps them onto newly-created vendors and best-effort fills empty columns on existing vendors (never overwrites user-populated fields). Email values are validated via the shared validateEmail helper; invalid values are dropped with a warning log. Extracted addresses now create a structured Address row instead of being stuffed into the free-text notes field.
VendorRepository write-path normalization — overrides create / createAll / updateById / updateAll / replaceById to collapse empty-string email / orderingEmail / accountNumber / phone / paymentTerms to null, so the PO send fallback (orderingEmail || email) can't land on "" and PATCH can explicitly clear a column.
Shared CREDIT_TERMS constant — mirrored in frontend/src/app/shared/constants/credit-terms.constant.ts and backend/src/constants/credit-terms.constant.ts, consumed by both edit-customer (refactored off its inline literal), the new edit-vendor, and the ingredient importer's normalizePaymentTerms helper so canonical values don't drift across the stack.
Vendor address lifecycle hardening — vendor controller uses delete-first-then-update semantics when a client clears the address (prevents orphans if the Vendor update fails); orphan-cleanup failures on create / clear / replace are escalated to Sentry with tags: { context: 'vendor-address', kind, vendorId, addressId } per the project Sentry convention. Address payload on create now requires address1 + city minimum (aligned with importer strictness) — partial country-only or postal-only payloads are rejected with HTTP 400 instead of polluting the Address table.
Ingredients bulk editor: explicit save/discard pattern — replaced auto-save-on-debounce with a Save/Discard banner (matching the recipes bulk editor UX). A dirty-state service (IngredientBulkEditorChangesService) tracks which rows have changed and emits a reactive count. Changes are batched and saved sequentially; partial failures leave failed rows dirty so users can fix and retry without losing context. Navigation away with unsaved changes triggers a confirmation dialog via the existing UnsavedChangesGuard.
Ingredients bulk editor: Par stock and Track inventory columns — IngredientInventoryConfig records (separate API model) are batch-fetched after each page load and merged into the row data. Par stock accepts a number input; Track inventory uses a toggle switch. Both fields participate in dirty tracking and are saved (or upserted if no config exists yet) alongside the ingredient on batch save. A skeleton/spinner is shown in those cells while configs are loading.
Ingredients bulk editor: 3-state status selector — replaced the Active/Inactive toggle with um-status-selector [includeDraft]="true" so Draft status is now selectable directly in the bulk editor.
Ingredients bulk editor: Label name moved to Procurement group — Label name column relocated from the Core attribute group to the rightmost position in the Procurement group, matching the edit-recipe form layout.
Search and filters in bulk mode (Ingredients) — when Typesense search is active, the Typesense table is kept alive via [hidden] (instead of destroyed via *ngIf) so its data feed continues in bulk mode. A dedicated filter strip (search input, Status, Ingredient Categories dropdowns, Reset button) is shown above the bulk editor. Filter controls lock (opacity + pointer-events: none) when unsaved changes are present to prevent silent data loss from a page re-fetch.
Search and filters in bulk mode (Sub-Recipes / Products) — the existing filter bar (Status, Recipe Categories, Workflows) is already rendered outside the list/bulk mode guard, so it remains visible and functional in bulk mode. Filter controls lock identically when unsaved changes are present.
Products (Meals) bulk editor: Default retail price and Default wholesale price columns — two number inputs (step 0.01, min 0) appear in a Pricing column group only when recipeTypeName === 'Meal'. Both fields are tracked for changes, restored on Discard, and included in the batch save payload.
Fixed
PO Export PDF / Export Excel from the single-PO 3-dot menu (#1248) — staging was 500'ing because every Export (even single-PO) routed through the async Cloud Tasks + GCS batch pipeline, and two infra gaps hid the real cause: the Cloud Tasks service agent lacked roles/iam.serviceAccountTokenCreator on the backend SA (so dispatch failed before enqueue), and /internal/process-batch was rejected by the app-level authorize middleware with a 403 before its own OIDC verification ran (loopback4-authorization is secure-by-default when no @authorize decorator is present). Fixes: (1) frontend routes single-PO Export PDF/Excel through the sync GET /purchase-orders/{id}/export endpoint (purchase-order-edit.component.ts:downloadPdf/downloadExcel → poApi.exportPo() blob download) — works cross-cloud via utils/gcp-auth.ts, no Cloud Tasks needed; batch pipeline preserved for multi-PO list selections. (2) backend/src/application.ts extends AuthorizationBindings.allowAlwaysPaths to cover /internal/process-batch, /internal/reap-stale-jobs, /internal/record-ai-feedback, /internal/process-ai-feedback-export, /internal/purge-ai-feedback (inline comment warns that the library matches via req.path.indexOf(path) === 0 — future /internal/* routes MUST call verifyOidcToken as their first line). (3) BatchExportService catch block now writes Cloud Tasks failures to process.stderr alongside logger.error (band-aid — the Winston/Pino transport silently drops error-level lines on Cloud Run; full logger fix tracked for pre-cutover). (4) PurchaseOrderExportService.generatePdf rejects unresolved PDF_SERVICE_URL placeholders (strings starting with [) with a 503 fallback so AWS prod with unconfigured env vars gets a clean error instead of a DNS failure. Staging IAM grant applied; prod Cloud Tasks API enabled; remaining prod infra wiring (queue create, GCS bucket + TTL, env vars) documented in docs/infrastructure/gcp-cutover-runbook.md §P3f + P30 checklist. Verified by Gemini 3.1 Pro CTO (2 rounds).
Vendor edit save 422 on existing vendors (#1248) — PATCH /vendors/{id} was rejecting the nested address payload as an additional property because the request-body schema getModelSchemaRef(Vendor, {partial: true}) sets additionalProperties: false, even though resolveAddressId() inside the handler specifically reads and strips vendor.address before the repo write. Switched POST + PATCH to the permissive {type: 'object'} schema (matches the customer controller pattern) and added a focused validateVendorEmails() helper that re-enforces format: 'email' on email + orderingEmail — otherwise the loose schema would drop the model-level format constraint.
Empty-state table rows light up teal on hover — the global .table-hover tbody tr:hover rule in _light.scss:50 was colouring the <tr> that renders Empty list. inside every um-paginated-table, which implied clickable content where none exists. Added class="empty-row" to the template row and a scoped :host ::ng-deep override in paginated-table.component.scss that cancels the hover + focus-within backgrounds. Applies to every empty table in the app.
AWS prod boot-time schema sync restored — PR #1236 gated app.migrateSchema off by default for NODE_ENV=production (correct for GCP Cloud Run, where the pre-deploy migrate Job owns schema sync). AWS prod has no equivalent pre-deploy Job — boot-time migrateSchema has always been the only schema-sync mechanism. With AWS prod ~4 weeks behind on deploys (ai_insight_feedback table, goods receipt version/variance columns, vendor addressId FK, physical count notes, etc. all accumulated on main), a gated-off boot call would leave prod running without those new columns/tables and break any feature that expects them. Fix: set ENABLE_BOOT_MIGRATION: 'true' in backend/server-config/ecosystem.config.js env_production block. The existing env-sync machinery in backend/aws/after-install.sh auto-injects new keys into /var/pm2/ecosystem.config.js on every deploy, and backend/aws/stop.sh → start.sh runs pm2 kill && pm2 start $CONFIG_FILE --env production which fully reloads env vars. Hardened through Gemini 3.1 Pro CTO review (8 findings: 3 CRITICAL disarmed by codebase evidence, 3 HIGH disarmed or mitigated, 2 MEDIUM accepted). All new columns since the last prod deploy either have default: in the model or are nullable, so rolling CodeDeploy (3 EC2s, sequential) is forward-compatible — old code on EC2-2/-3 continues inserting successfully while EC2-1 runs the schema migration.
Migrate schema drop-recreate loop + prod schema parity — LoopBack's loopback-connector-mysqlexistingSchema:'alter' mode actively DROPs any DB index not declared in model.settings.indexes[] or named after a property with index: true (see migration.js:310-357). The boot-time app.migrateSchema call at backend/src/index.ts:54 was wiping every composite the post-schema scripts (migrate-vendor-address-and-index.ts, migrate-ai-insight-feedback.ts) created on every Cloud Run web service restart — including the UNIQUE idx_aif_idempotency Cloud Tasks at-least-once delivery guard. Fixes applied after 3 rounds of Gemini 3.1 Pro CTO review:
Gated boot-time migrateSchema behind ENABLE_BOOT_MIGRATION env var (backend/src/index.ts). Defaults ON for local dev (NODE_ENV ≠ production/staging) so fresh checkouts still bootstrap. Production/staging Cloud Run web instances no longer run DDL on boot, which also eliminates a metadata-deadlock hazard when multiple instances scale up concurrently. The pre-deploy migrate Cloud Run Job remains the sole owner of default-DS schema sync.
Re-declared the 5 ai_insight_feedback composites in settings.indexes using strict keys: {<DB column name>: 1} syntax and explicit options: {unique: true} on idx_aif_idempotency. PR #1233's earlier attempt used JS property names (idempotencyKey) and hit errno 1072 — the correct approach is literal DB column names (idempotency_key, created_on, etc.). All 5 composites fit within the 3072-byte InnoDB key limit so LoopBack's prefix-free buildIndexes() output is safe.
Vendor composites stay post-schema-only because tenantId(255) + subTenantId(255) + accountNumber(512) = 4088 bytes exceeds the InnoDB limit without prefix. Model comment documents the trade-off.
Vendor.addressId TEXT → BIGINT normalization in migrate-vendor-address-and-index.ts with 4 hardening safeguards: REGEXP anomaly guard (^[0-9]+$), UPDATE SET NULL WHERE TRIM() = '' before the cast (strict-mode-safe), information_schema.STATISTICS existence check before conditional DROP INDEX idx_vendor_address (MySQL has no portable IF EXISTS), and row-count warning log over 10K. Addresses schema drift on GCP staging and AWS prod where the column was created as TEXT by historical migrations.
New unit spec migrate-vendor-address-and-index.test.ts (6 tests, all passing) covers: table-not-found early return, BIGINT skip, non-numeric anomaly abort, clean TEXT → nullify + ALTER, pre-existing index drop ordering, and dry-run purity.
Typesense client: keep-alive + cached DNS to survive bursty reindexes — the Typesense Node client was initialized with additionalHeaders: { Connection: 'close' }, forcing a fresh DNS lookup + TCP handshake + TLS negotiation on every request. On GCP Cloud Run, a full reindex (~15k documents) exhausted the per-VPC Cloud DNS query budget, surfacing as ENOTFOUND getaddrinfo failures against a cluster that was healthy in the Typesense Cloud UI. Replaced with explicit http.Agent / https.Agent configured with keepAlive: true + a module-level cacheable-lookup resolver (60s TTL / 10s errorTtl) so bursts reuse sockets and share resolved addresses instead of hammering the VPC resolver. New unit test typesense-connection.test.ts locks the agent config + the absence of Connection: close against accidental regression. Adds cacheable-lookup@^6.1.0 to backend/package.json.
Founder Cockpit: session JWT now authenticates the metrics proxy — the cockpit was rendering "Task queue temporarily unavailable" because the Angular data service called the LoopBack proxy with raw HttpClient (no Authorization header), and the backend's @authenticate(STRATEGY.BEARER) gate returned 401. Switched FounderCockpitDataService to ApiService.apiRequest so the user's session JWT is attached automatically (same pattern every other admin service uses). Also dropped the leading /api/ from the LoopBack route; apiURL already prefixes /api, so the controller now registers /cockpit/metrics and the frontend calls cockpit/metrics. New backend/src/controllers/cockpit.controller.ts added.
Founder Cockpit: resilient error handling across both data methods — getPendingTasks() now has its own catchError returning an empty task list (matching its independent-error-boundary JSDoc contract), and the shareReplay(1) error reset (metrics$ = null) moved from getDashboardData into getMetrics itself so both methods recover after a proxy failure. Backend upstream-body validation hardened with Array.isArray() guard. Spec rewritten from HttpClientTestingModule to jasmine.createSpyObj<ApiService> with all prior assertions preserved plus a new retry-after-error test.
Vendor screen overhaul: staging post-deploy fixes (hotfix for the Added entry above) — four UI issues surfaced on GCP staging after the vendor overhaul shipped: (1) modal close X clipped by a w-100 wrapper in the header, (2) address autocomplete rendered expanded (mockup called for collapsed-by-default), (3) Vendor name label missing the required-field asterisk, (4) new-vendor "Add" form opened in readonly mode because GenericEditComponent.readonly defaults to true and form controls stayed hidden behind *ngIf="!readonly". Fixed by dropping the w-100 wrapper, adding [collapsed]="true" to <um-address-input>, adding the required class to the label, and a tap in itemLoaded that flips readonly = false when !updatedItem.id. Two regression tests added in edit-vendor.component.spec.ts.
GCP staging deploy workflow: migrations now run before service replace — the vendor overhaul surfaced a class of 500s where new backend code went live against an un-migrated schema (Unknown column 'addressId'). deploy-gcp-staging.yml now derives a upmeals-backend-migrate Cloud Run Job from the patched service YAML (inheriting image, env vars, secret refs, cloudsql-proxy sidecar, and VPC network — annotations pulled from the service spec so config drift can't strand the Job) and runs it with --waitbeforegcloud run services replace. maxRetries: 1 absorbs transient Cloud SQL blips without hiding real failures (LoopBack migrations are idempotent).
Inventory module: 20-bug QA batch (data correctness, realtime sync, multi-tenancy, modal parity) — resolved a full QA pass of the goods-receiving / PO modals shipped in #1217, #1220, #1223:
Pack description format aligned across FE and BE — backend generatePackDescription was producing "12 x Gram" / "Case" strings while the frontend's IngredientProcurement.getNiceNameDetails() renders "120 g box, case of 12". Ported the FE algorithm to the BE (uses shelfAmount + shelfUnit.symbol + purchaseUnit.name, plus master-case form when quantityPerPurchaseUnit > 1). Added a one-shot backfill-pack-description.ts script (tenant-aware, --dry-run support, batched 500 rows) to rewrite existing rows. Script is designed to run as a GCP Cloud Run Job.
Purchase-order realtime sync to Receiving list — submit() now publishes an SSE event via Redis-backed EventBusService, and all PO/goods-receipt event model names are normalized to lowercase ('purchaseorder', 'goodsreceipt') to match the frontend filter. Goods-receipt service now publishes on create/update/confirm so partial-received state propagates across tabs without refresh.
Submitted-by attribution — added submittedBy JSON column to PurchaseOrder; submit() endpoint resolves the current user via resolveUserIdentity() and stores them alongside submittedAt. Rendered on the PO view as a "Submitted by" row.
createDraft payload completeness + date validation — frontend was dropping expectedDeliveryDate and notes on first save. Both fields now round-trip through the FE service, BE controller (with ^\d{4}-\d{2}-\d{2}$ pattern validation), and BE service.
Ship To tenant label — PO detail response now includes tenant.name and subTenant.name via @belongsTo relations; the Ship To block renders "UpMeals — Vancouver" from the PO's persisted relations (not session state), preserving historical integrity if users later switch subtenants.
Receiving list: per-row action state + trackBy — clicking Receive on one PO no longer greys out every other row's button; trackBy on purchaseOrderId prevents full list re-render on state changes.
Typesense refresh icons — added standard sync-alt refresh buttons to PO list and Receiving list headers, with [disabled] + fa-spin during fetch to prevent spam-click.
Goods Receiving modal polish — modal header now matches the app-wide modal-header pattern; Quick Receive banner toned down from neon teal to neutral gray with a teal accent stripe; variance panel softened from alarm-yellow to neutral "Reason (optional)" prompt; entering a short-received qty keeps the row checked (variance surfaces independently); qty input validates min=0 on FE + BE; Receive All no longer auto-saves (explicit Save Draft with subtle pulse when dirty).
PO create/edit modal — unit cost is now read-only in the line-items table (cost is procurement-level); live Order Subtotal in the sticky footer; Export (PDF/Excel/Print) moved out of the 3-dot menu into a dedicated dropdown beside Mark as Sent; Mark as Sent tooltip relocated to a keyboard-focusable info-circle icon with a shorter copy.
Duplicate units in recipe editor dropdown (GCP staging only) — unit selector showed 4× each symbol (g, oz, etc.) on GCP staging. Root cause: the Calgary ETL (backend/src/scripts/calgary-migration/) blindly INSERT'd globally-shared reference tables (Unit, RecipeType, Language) in Wave 1 as if they were tenant data, duplicating UpMeals' rows (148 duplicate Unit rows + 6 duplicate RecipeType rows on staging; AWS prod untouched). Fix has three parts: (1) new globalReferenceTable: { naturalKey } flag on TableConfig in wave-config.ts; migrate-table.ts now UPSERT-merges these via natural-key lookup and seeds idMap without insert — prevents recurrence on prod cutover. (2) ER_DUP_ENTRY handler in migrate-table.ts now seeds idMap for non-auto-increment PKs (fixes silent idMap gap for Language). (3) New one-shot script backend/src/scripts/cleanup-duplicate-global-reference-rows.ts repoints FK references from duplicate IDs to lowest-id canonicals (chunked UPDATEs at ≤5k rows/statement) and adds UNIQUE INDEX (naturalKey, deleted_on) as defense-in-depth. validate-migration.ts extended with a post-ETL duplicate-key check. Staging cleanup is user-initiated; no prod impact.
Bulk editors: search, filters, and header fixes across all three pages — resolved display and functional bugs introduced in the initial bulk editor enhancement:
Ingredients header controls in bulk mode — the full Actions dropdown, Import Ingredients button, and Add Ingredient button were visible in bulk edit mode. Replaced with a standalone "Exit bulk editor" button matching the sub-recipes/products pattern.
Missing search/filters on Sub-Recipes and Products bulk editors — added the search bar, Status filter, and Category filter to both pages. The Typesense table is now kept alive via [hidden] (instead of destroyed via *ngIf) so search queries work in bulk mode.
Ingredients bulk search returning no results — the Typesense table's updateQueryParams() was changing the URL, triggering the parent's route handler to overwrite search results with unfiltered API results. Added suppressQueryParamUpdates input and skipQueryParamUpdate parameter to prevent URL updates in bulk mode.
DRY refactor — moved bulk filter state and methods (search, status, category filters with debounced search subject) from IngredientsManagementComponent to the shared GenericManagementComponent base class. Recipes override rebuildBulkExternalFilter() to use the correct Typesense field name per recipe type (productCategoryName for Meals, recipeCategoryName for Sub-Recipes).
Recipes pagination layout — restructured to match the ingredients pattern (removed unnecessary wrapper divs, fixed mr-sm-4 placement, added null safety on the guard condition).
Recipes status column width — widened from col-width-8 to col-width-10 so the status dot and full text display without truncation.
Goods receiving: modal footer clipped by unconstrained Angular host (C1 hotfix for #1220) — the scoped overflow: hidden on .modal-content + overflow-y: auto on .modal-body landed in #1220 but never engaged because um-goods-receiving renders as a plain display: block host between them, breaking the flex chain. Body grew to its intrinsic 1335 px and the footer (Cancel / Save Draft / Confirm Receipt) was clipped below the 900 px viewport on any PO with >~6 line items. Added :host { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; max-height: 100%; } in goods-receiving.component.scss so the host is a sized flex parent of the body. Verified on staging-gcp (PO-00003, 13 items @ 1440×900).
Goods receiving: summary endpoint didn't mark draft-receipt POs as in_progress (C2 hotfix for #1220) — #1220 updated the row-state logic in receiving-list.component.ts so the list button flips to "Continue" when receivingStatus === 'in_progress' && draftReceiptId, but the receiving-summary SQL only derived receivingStatus from po.status — a draft receipt against a submitted PO stayed 'upcoming', so the button stayed "Receive". PurchaseOrderRepository.getReceivingSummary CASE now returns 'in_progress' when EXISTS (draft GoodsReceipt) regardless of po.status, and the overdue / upcoming filter clauses exclude POs with drafts so one PO can't appear in two buckets. Added regression test in backend/src/__tests__/unit/repositories/purchase-order.repository.test.ts.
Goods receiving: post-ship QA parity pass — closed 16 findings surfaced by design review of the shipped modal against the approved mockup:
Modal body overflow clipped the footer — global .modal-dialog-scrollable .modal-content { overflow: visible } rule (used to unclip ng-select dropdowns) caused .modal-body to grow past its container. Added a scoped .goods-receiving-modal override in _modal.scss that restores overflow: hidden on the content and overflow-y: auto on the body so line items scroll internally and Save Draft / Confirm Receipt remain pinned at the bottom.
Duplicate POST /api/goods-receipts on repeat "Receive" clicks — startReceiving() unconditionally POSTed a new draft. It now branches: if item.draftReceiptId is already populated (a prior click created a draft), it opens that draft via GET instead. After a successful create, draftReceiptId and receivingStatus are updated locally so the row's button flips to "Continue" without a list reload.
Variance reason incorrectly required — the approved mockup specifies variance reasons as optional, but hasMissingVariances() was wired into the Confirm Receipt disable gate. Removed from the disable logic; the variance panel still surfaces so reasons can be captured when useful.
"Quick Receive" banner disappeared after the first typed qty — banner now stays visible while fullyReceivedCount < rows.length and swaps its copy + CTA to "Receive Remaining" once some items are confirmed.
iPad landscape stuck on desktop-sized touch targets — the single @media (max-width: 768px) breakpoint missed iPad landscape (1024–1366 px). Switched to @media (hover: none) and (pointer: coarse) (plus a 768 px fallback) so all touch devices get the 44 px checkboxes, qty buttons, inputs, and footer buttons.
Missing mockup context fields — the context card now shows vendor Contact / Email / Phone and Order Date (pulled from Vendor.mainContactName/email/phone and PurchaseOrder.orderDate). Backend GET /goods-receipts/:id was extended to include these alongside the existing vendor/PO context.
Expected qty column wrapping / misaligned digits — white-space: nowrap + font-variant-numeric: tabular-nums on the expected-qty span.
Qty control redesign — removed the increment (+) button to match the mockup layout (Expected | input | single decrement); receiving is rarely over-qty, adjustments are downward.
Progress counter now counts fully received items — replaced entriesCount (any qty > 0) with fullyReceivedCount (qty equals expected), and surfaced a "N with variance" note in the footer when partial rows exist.
Unit suffix on cost — line-item subtitle now renders $32.00/case when the backend returns a purchase unit name (batched unit lookup in findById).
Dates humanized — receipt date, expected delivery, and order date all run through | date:'mediumDate' (e.g. Apr 15, 2026).
Skeleton loader parity — skeleton rows now include a qty-group column; the loading state shows um-skeleton-button placeholders for the footer actions.
Checkbox keyboard parity — the custom receiving checkbox now activates on both Space and Enter.
Modal width — switched from modal-xl (1440 px, wasted gutter on wide screens) to modal-lg (1152 px) to tighten the single-column list density.
Dead code — removed the unused getProgressPercent() helper on ReceivingListComponent (the value is already denormalized onto each row during mapping).
PO List: Export button dropdown affordance — removed the Bootstrap dropdown-toggle CSS pseudo-caret (too subtle to communicate "this is a dropdown") and replaced with an explicit fa-icon chevron-down at the end of the button label.
PAR Level read-only display: removed ~ approximation prefix — roundForDisplay() was prepending ~ to non-integer values (e.g. ~1.1 Cases). The tilde added visual noise without meaningful precision signal; values are now displayed as plain decimals (e.g. 1.1 Cases).
Inventory UI design review — cross-cutting polish — resolved 13 design findings across the inventory module:
Back navigation buttons — all three sub-pages (Goods Receiving, Physical Count Detail, Physical Count List) now use the standard btn-link + text-primary + fa-icon chevron-left pattern, matching the rest of the app. The error-state back button in Goods Receiving was a secondary button with HTML entity arrow and was corrected to the same pattern.
Vendor dismiss undo: fixed a silent no-op bug where the undo toast "restored" a dismissed vendor recommendation but nothing reappeared. The root cause was that rebuildRecommendationsAfterDismiss() was filtering from the already-mutated recommendations array (vendor already removed). Introduced allRecommendations as an immutable source populated at load time; rebuild now filters from the full source so undo correctly restores the vendor.
Reorder dashboard dead CSS — removed the .btn-edit-pars custom button class (overriding primary border/color on a secondary button) and the .flex-grow local CSS class (replaced with Bootstrap's standard flex-fill).
Stock adjustment modal — current stock value was wrapped in a fake form-control input element. Replaced with a plain span.display-xs to avoid a misleading editable-field appearance.
Physical Count List: inline style removed — style="max-width: 320px" on the first-count description paragraph replaced with .first-count-description SCSS class.
Physical Count Detail: filter dropdown — the custom <select> and associated hand-rolled CSS were replaced with the standard um-filter-dropdown component. Dead filter-pill and filter-area SCSS blocks removed.
Physical Count Detail: close button — the × character close button replaced with the btn btn-link + fa-icon times pattern.
Physical Count Detail: heading hierarchy — section heading changed from <p> to <h2 class="text-sm font-weight-semibold"> with corresponding SCSS update.
PO List: dead print button removed — the *ngIf="isExportingBatch" print-action block that was always hidden (only shown while exporting) removed along with its stale boolean.
PO Edit: Bootstrap spacing tokens — p-3, p-2, gap-2, mb-3, gap-3 replaced with Untitled UI tokens p-md, p-sm, gap-sm, mb-lg, gap-md throughout the edit modal.
Goods Receiving: search filter — um-search-input added above the receiving table. Filtering uses a cached filteredRows property (not a getter) updated by applySearchFilter() at all mutation points (load, search change, receive-all) to avoid DOM thrashing. Progress count and isDirty continue to operate on the full rows array.
Goods Receiving: "Receive all" and back-nav link styles — text-primary added to "Receive all" link button; back-nav buttons corrected to text-primary text-nowrap.
Start count spinner — mr-1 Bootstrap class replaced with mr-xs Untitled UI token; "Starting..." loading text added to match the Physical Count List button.
Dynamic Typesense connection for local GCP development — updated the backend /search/scoped-key endpoint to return the Typesense host URL alongside the scoped API key. The frontend TypeSenseService now dynamically updates its base URL from this response. This fixes "401 Unauthorized" errors when local development is configured to use a remote staging Typesense instance (where the key is signed for staging but the frontend was hardcoded to localhost).
Auto-generate PO list not refreshing without F5 (BUG-A) — handleAutoGenerateResponse() in PurchaseOrderListComponent was missing a store.clearCache() call before reload() in the PoGenerationReason.Success case. All three bulk action paths (submit, receive, cancel) already called clearCache() correctly; the auto-generate path was the only gap. After generating POs, the list now immediately reflects new entries without a manual page refresh.
"Receive Goods" blocked by 409 on orphaned draft receipt (BUG-B backend) — createReceipt() in GoodsReceiptService was throwing 409 Conflict when a draft receipt already existed for a purchase order (e.g., an abandoned receive-goods session). The endpoint is now idempotent: if a draft receipt exists it is returned directly rather than blocking. A structured warning is written to GCP Cloud Logging if the PO was updated after the draft was created (modifiedOn staleness check) so the discrepancy is visible during operations review.
Receipt creation errors showing generic message instead of backend detail (BUG-B frontend) — the receiveGoods() error handler was ignoring the API response body and always showing a hardcoded string. It now uses getErrorMessage() to surface the actual backend error message (e.g., "A draft receipt (#5) already exists") while falling back to a clear generic message for network-level failures.
PATCH 400 silently collapsing edit mode with no feedback (BUG-C) — when updateDraft returned a 400, the edit form appeared to freeze in a read-only state with no toast because: (1) disableEdit() is called before the API request in the base class, and (2) the child's doApiSaveRequest override had no catchError. Added catchError to the PATCH branch that calls errorHandler() (base class: danger toast + enableEdit() restore + isSaving=false) and GenericStoreService.clearStoresForModel() to invalidate the PO list cache. Returns EMPTY for graceful stream completion so re-clicking Save still triggers the API call.
Delete cancelled PO using soft-delete instead of hard-delete — DELETE /purchase-orders/{id} was delegating to SoftCrudRepository.deleteById() which sets deleted=true, meaning cancelled orders were hidden from listings but still existed in the database. The endpoint now uses deleteAllHard({id, status: 'cancelled'}) inside a READ_COMMITTED transaction, atomically enforcing the status guard to prevent TOCTOU races where a concurrent mutation could change status between the check and the delete. Line items are hard-deleted in the same transaction.
"Copy previous PO" triggered while catalog still loading — the copy-previous action in create-mode PO modal would show a "No previous PO found" toast if clicked before the forkJoin fetching vendor catalog and last-PO data completed. Added an isLoadingCatalog guard that shows "Still loading vendor data, try again in a moment" instead of the false-negative toast.
Delete button visible to read-only users — the delete button in the PO edit modal and the delete row action in the PO list were rendered for users without edit permissions. Both now gate on canEdit before checking the cancelled status.
Double-click on "Receive Goods" creates duplicate receipts — clicking "Receive Goods" rapidly in the PO list triggered multiple concurrent receipt creation requests. Added a processingReceiptIds Set that blocks re-entry per PO ID until the request completes (with finalize cleanup).
Blob URL memory leak in print actions — printOrder() and printCount() create blob URLs for PDF preview and schedule a 60-second revokeObjectURL via setTimeout. If the component was destroyed before the 60s elapsed, ngOnDestroy cancelled the timer but the blob URL was never revoked. Both components now track activePrintUrl and revoke it in ngOnDestroy.
Physical count list: API error kills the reload stream permanently — the error handler in the outer subscribe() call terminated the reload$ Subject-based stream on any API error, making filters and date-range changes stop working for the rest of the session. Moved error handling inside switchMap via catchError(() => of([])) so the stream stays alive on transient failures.
Physical count detail: changing unit initializes countedAmount to 0 instead of null — onUnitChange() set item.countedAmount = 0, which displayed "0" in the input but left isExplicitlyCounted = false, causing the variance calculation to show 100% discrepancy for every item the user switched units on. Setting to null correctly empties the input and signals "not yet counted".
Print opens blank page in Chrome's PDF viewer — win.print() was called immediately inside the load event handler, but Chrome's built-in PDF viewer renders its content asynchronously after the document loads. A 500 ms delay is now added before triggering the print dialog, giving the viewer time to initialize. Applies to both the PO edit modal and the physical count list.
Physical count: ingredients with inner pack but no case unit default to recipe shelf unit — the unit priority chain in physical-count-detail was skipping procurementUnitId (inner pack / each), so ingredients that had a procurement unit configured but no purchase unit (case) fell through to the shelf unit (e.g. ml, g). Counting in recipe measurement units is impractical for physical inventory; the chain now tries purchaseUnitId → procurementUnitId → shelfUnitId when no countedUnitId is already saved.
Physical count: theoretical unit defaults to purchase/case unit instead of shelf unit — the unit dropdowns on the physical count detail page (both Theoretical Unit and Counted Unit) defaulted to the shelf unit (e.g. Pound, Gram) on page load, even when a purchase unit (Case, Each) was configured. This made counts impractical because staff count in cases, not grams. The priority chain now selects purchaseUnitId first when it differs from the shelf unit, falling back through procurementUnitId to shelfUnitId. Counted Unit is tracked independently so staff can count in a different unit than the theoretical display.
Physical count: vendor column XSS vulnerability — the Vendor column cell was built with html: \${item.vendorName} ...\` string interpolation passed as raw HTML into the paginated table. Replaced with a #vendorTplng-template` using Angular interpolation binding, which safely escapes all vendor name and code values.
Inventory stock list: missing inventory value column — the stock list PDF export did not include a computed inventory value (on-hand quantity × cost per unit). Added inventoryValue field to the export computed from (costPerPurchaseUnitUSD / shelfAmount) * onHandAmount, with a running total row at the bottom of the table.
Readonly PO modal header missing payment terms and vendor account number — when viewing a submitted or received purchase order, only the vendor name and order date were shown in the modal header subtitle. Payment terms and vendor account number are now displayed as a second subtitle line when present on the purchase order.
PO batch export 500 on GCP staging: wrong Cloud Tasks region default — BatchExportService.dispatchCloudTask() defaulted GCP_LOCATION to us-central1, but Demi's GCP infrastructure is in northamerica-northeast1. Any environment without GCP_LOCATION set would build a queue path pointing at the wrong region, causing a NOT_FOUND / PERMISSION_DENIED from Cloud Tasks and surfacing as a 500 on POST /purchase-orders/export-batch. Fixed the default to northamerica-northeast1 (matching every other Cloud Tasks service in the codebase). Also added a logger.warn at dispatch time if GCS_EXPORTS_BUCKET is not explicitly set so the fallback bucket name is visible in logs before the worker attempts to upload. Added scripts/setup-po-batch-export-infra.mjs to provision the Cloud Tasks queue, GCS bucket, and print IAM / env var steps for staging and production environments.
Goods receiving: quantity inputs showing "0" for unstarted receipts — editQty was initialized to item.receivedQuantity || 0, so every fresh draft showed "0" pre-filled in all quantity cells. Changed to initialize null when receivedQuantity === 0, which renders the inputs as blank with a "0" placeholder. isDirty, snapshots, and payload coercion updated to treat null as zero throughout.
Goods receiving: draft badge showing gray instead of warning amber — badge-gray was used for the "Draft" status badge, inconsistent with the physical-count pattern where badge-warning is the standard for in-progress states.
Goods receiving: missing "Receive All" button — the receiveAll() method existed but had no UI entry point. A "Receive all" link button is now shown above the table when the receipt is in draft mode, filling all quantities to their expected order quantities.
**Goods receiving: table rendered outside *ngIf guard** — um-paginated-table was placed outside the *ngIf="!isLoading && receipt" guard, so an empty table skeleton was always rendered even on load error. The table, action bar, and buttons are now all inside the guard. An error/empty state is shown when the receipt fails to load.
Goods receiving: skipped items in confirmation result show IDs instead of names — after confirming a receipt, skipped items were listed only by numeric ingredientId. They now show the ingredient name (cross-referenced from the loaded rows) plus the reason for skipping.
Goods receiving: optimistic rollback not restoring dirty-guard snapshots — when a saveDraft API call failed, the rollback restored originalQtys but not savedQtySnapshot / savedNotesSnapshot. This left isDirty permanently returning false after a failed save, silently allowing navigation without a warning. Both snapshots are now captured before the optimistic update and fully restored on rollback.
Goods receiving: receiveAll() not re-rendering table inputs — mutating row objects in-place without spreading the array reference left um-paginated-table unaware of the change, so quantity inputs displayed stale values. receiveAll() now spreads this.rows after updating quantities.
Added
Prospect Funnel in Founder Cockpit — new "Prospect Funnel" and "Conversion Rates" cards on the Cockpit dashboard. Paginated HubSpot contact search (up to 5,000 contacts, 50-page cap with ERROR log on overflow) filtered by all demi_outreach_stage values. Stage pills rendered by cadence order (teal = done, amber = due today, grey = pending). "Due today" badge shows contacts where cadence step falls exactly on today's Vancouver date (DST-safe UTC arithmetic). Conversion metrics: Reply Rate %, Demo Booked Rate %, and Reply→Demo % with progress bars and industry-benchmark badges (5–10%, 2–5%, 30–50%).
Contact CRM card — new GET /hubspot/cards/contact-context endpoint + contact branch in DemiSalesOsCard.tsx. Renders lead score, location count, tier tag (green/yellow/default), outreach stage, next follow-up due date (red if overdue), and research notes (truncated to 200 chars). Follows the same auth pattern (isHubSpotBrowserRequest) and error-retry UX as the existing deal card. Place the card via HubSpot → Settings → Objects → Contacts → Record Customization.
Prospect cadence scanner — new POST /internal/scans/prospect-cadence endpoint. Runs 5 parallel per-stage HubSpot searches with server-side date filters (demi_outreach_start_date LTE cutoff). Idempotency via demi_last_cadence_step_created contact property — skips contacts where the current-stage task was already created; logs ERROR with remediation note if the idempotency PATCH fails after task creation. Batch task creation (10 at a time, 200ms pacing). WARN log when per-stage result hits the 100-contact cap. Requires the demi_last_cadence_step_created contact property to be created in HubSpot before first run (see deploy runbook).
Cloud Scheduler cadence scan job — hubspot-prospect-cadence-scan added to setup-cloud-scheduler.mjs. Runs weekdays at 8 AM Vancouver time (before SLA scan at 9 AM). attemptDeadline: 300s with 2 retries and 30–120s backoff. Run GCP_PROJECT_ID=upmeals-staging node scripts/hubspot-sales-os/setup-cloud-scheduler.mjs --apply to provision.- 30-day CC-required trial with Stripe Embedded Checkout — onboarding wizard gains a new Step 5 that mounts Stripe's embedded checkout form inline (no redirect). Users enter a credit card at signup; the trial is $0 today and auto-bills at trial end if not cancelled. Applies to both email and Google OAuth signups.
New POST /billing/checkout-session backend endpoint (JWT-derived tenantId only — IDOR-safe). Idempotency guard returns { alreadySubscribed: true } if an active subscription already exists. Creates a Stripe Embedded Checkout session with payment_method_collection: 'always' and 30-day trial.
DEFAULT_TRIAL_DAYS env var (default 30) controls trial duration across all subscription creation paths.
Frontend polls refreshMe (10 × 2s) after Stripe's onComplete fires to confirm webhook delivery before advancing.
Orphaned-tenant guard: tenants with signupFlowComplete=true but no subscription are redirected to /onboarding?step=5 instead of the dashboard (owner + admin only — team members pass through).
@stripe/stripe-js upgraded from 1.54.1 → 9.1.0 (createEmbeddedCheckoutPage requires v9+).
Step indicator updated to 6 dots; team invites moved to Step 6; processing screen to Step 7.
Founder Cockpit internal dashboard (GCP staging only) — new /founder-cockpit page gated behind showFounderCockpit environment flag (true only on GCP staging) AND ManagePlatformSettings permission (drew@upmeals.ca only). Displays: 4 KPI cards (pipeline value, overdue next steps, meetings booked 7d/30d, meetings completed 7d/30d with period toggle), pipeline stage bar chart by deal stage, revenue forecast line chart (weighted vs best case), closed-lost reasons donut chart (last 90 days), and Founder Review Queue task list (pending [Demi OS Review] HubSpot tasks). All data sourced from the Sales OS control plane (GET /hubspot/cards/founder-cockpit/dashboard + GET /hubspot/cards/founder-cockpit). Falls back to stub data while real control plane endpoints are deployed. Refresh button re-fetches all data without skeleton flicker. 14-test spec file covers data loading, chart building, tab switching, and trackBy helpers.
HubSpot Founder Cockpit dashboard — self-contained HTML dashboard at GET /cockpit on the Sales OS control plane. KPI tiles: active pipeline total, overdue next-steps count, meetings booked/completed (7-day and 30-day). Pipeline-by-stage bar chart and 90-day revenue forecast line chart via Chart.js. Closed-lost reason breakdown donut. Pending [Demi OS Review] task table. Token-gated via Bearer auth with session-storage persistence. Server-side metrics cache (5-minute TTL) with in-flight Promise deduplication to prevent concurrent requests from each firing independent HubSpot API calls and triggering 429 rate limits. Stale-while-revalidate fallback if HubSpot is temporarily unavailable. GET /cockpit/api/metrics JSON endpoint for programmatic access.
HubSpot inbound deal router workflow script — scripts/hubspot-sales-os/setup-hubspot-inbound-workflow.mjs creates or updates a HubSpot PLATFORM_FLOW workflow ("Demi Inbound Deal Router") that calls the control plane's /hubspot/workflow-webhook/inbound-routing webhook when a deal is created. Supports --dry-run (default), --apply, and --status modes. Handles create vs. update idempotently via PUT full-replacement.
Cloud Scheduler inbound routing scan — added to scripts/hubspot-sales-os/setup-cloud-scheduler.mjs as a fallback polling job (every 2 minutes, 4-minute lookback) for environments where the HubSpot Workflow trigger is not yet configured. retryCount: 0 prevents cascading queue backups given the 2-min schedule interval; Firestore idempotency prevents duplicate deal routing between poll cycles.
PO bulk actions toolbar — select up to 10 purchase orders from the list to Mark as Sent, Mark as Received (auto-fills received = ordered quantity for clean deliveries), or Cancel POs in one pass. Reuses the <um-bulk-action-toolbar> component used by orders/recipes/ingredients management. Pre-flight validation blocks the action and shows a specific error when POs have missing expected delivery dates (Mark as Sent) or are in an incompatible status (Mark as Received). Partial-failure responses are surfaced per-PO. Max 10 POs per bulk action enforced at both frontend and backend. Three new backend endpoints: POST /purchase-orders/bulk-submit, POST /purchase-orders/bulk-mark-received, POST /purchase-orders/bulk-cancel — each with per-PO transaction isolation and audit logging.
AI Feedback Browser admin tab UI (PR 5 of 7) — new ai-feedback component under Settings > AI Feedback tab (gated by ManagePlatformSettings). KPI row: Total Votes, Up:Down Ratio, Top Rejected Type, Most Active Tenant. Filter row: um-date-range-selector (default last 30 days), tenant and insight-type um-filter-dropdown, sentiment toggle, debounced free-text search. Paginated table via um-paginated-table with sentiment pills, recipe name links, Details button, and point-in-time tooltip. Async CSV/XLSX export: POST job → poll → open GCS signed URL. um-skeleton-text/um-skeleton-button during load; um-empty-state for zero-results. (PR 5 of 7 — requires PR 6 backend endpoints to be deployed first.)
Admin backend for AI insight feedback browser (PR 6 of 7) — new AiInsightFeedbackAdminService with cross-tenant findForAdmin (paginated, free-text q LIKE search with %_\\ escaping, date/sentiment/insightType/userId/tenantId filters), getSummary (total/up/down counts, upRatioPct, per-type breakdown, top-10 tenants), createExportJob (dispatches Cloud Task with 1500ms timeout; returns {batchJobId} immediately), and processExportJob (atomic Pending → Processing transition, GCS upload, 24h signed URL, retry-safe). Extends AiUsageAdminController with four new endpoints under /admin/ai-usage/feedback. Adds POST /internal/process-ai-feedback-export to InternalAiFeedbackController. BatchJob.type enum extended with ai-feedback-export-csv / ai-feedback-export-xlsx. All reads use bypassTenantIsolation: true. 26 new unit tests. (PR 6 of 7 in the AI insights hardening initiative.)
Extended accept/reject insight payloads with full feedback context (PR 4 of 7) — InsightFeedbackPayload interface defined in product-insights-api.service.ts with idempotencyKey (UUIDv4, crypto.randomUUID()), insightType, recipeId, recipeName, sourceIngredientId/Name, targetIngredientId/Name, costDeltaCents. Both acceptInsight and rejectInsight updated to the typed interface. Panel component builds payloads via buildBaseFeedbackPayload() helper; sentinel 0 ingredient IDs excluded. ai-insights.component.ts updated in parallel. Backward-compat value/impact fields preserved for ai_preference_memory. (PR 4 of 7 in the AI insights hardening initiative.)
Performance
GET /recipes/:id/costing-data — 24s → ~2s cold-cache latency fix: Two bugs in CostingService.calculateRecipeCostPer100UnitsRecursive() caused every request to recalculate all sub-recipe costs from scratch in serial. Bug 1: the Redis cache read/write guard was level === 0 only, so sub-recipe costs (levels 1+) were never read from or written to Redis — each request started fresh regardless of cache state. Bug 2: the sub-recipe loop was for...of await (serial), so 3 sub-recipes at ~8s each cost 24s worst-case. Fixes: (1) remove the level === 0 Redis guard — all recursion levels now read/write Redis; (2) replace the serial loop with Promise.all + pLimit(3) for bounded parallel resolution (fresh pLimit instance per invocation to prevent parent/child deadlock); (3) request-scoped memo (Map<string, Promise<CostingInfo>>) deduplicates identical sub-recipe calculations when the same recipe appears in multiple branches of the tree (DataLoader pattern — promise registered synchronously before first await to prevent concurrent siblings racing past the has() check); (4) undefined recipeInstance.id guard prevents recipe:undefined:... cache-key collisions on unsaved draft recipes; (5) Redis read failures treated as non-fatal cache misses rather than propagating as 500 errors. Observed improvement on Recipe 4462 (Almond Chicken Bites): 24s → ~2s cold cache.
Changed
Trial duration 14 → 30 days — all subscription creation paths (createSubscription, createDefaultSubscriptionForCurrentTenant, new checkout session endpoint) now default to 30-day trials via DEFAULT_TRIAL_DAYS env var.
Pricing tier copy — "14-day free trial" updated to "30-day free trial"; "No credit card required" updated to "Cancel anytime before trial ends" to reflect the new CC-required flow.
Signup page subtitle updated to "Start your 30-day free trial".
POST /purchase-orders/{id}/submit — breaking API contract change: Response status changed from 204 No Content (void) to 200 OK with the full updated PurchaseOrder entity. The submit endpoint is now hardened with a compare-and-swap (updateAll predicate + exclusive InnoDB row lock) to prevent double-submit races; cross-tenant PO IDs surface as 404 (previously could surface as 409); 409 Conflict is now returned instead of 400 Bad Request when the PO is not in Draft status. Any caller that checks for 204 or discards the response body must be updated.
TenantCurrencyService + UmCurrencyPipe — fix currency inconsistency on inventory pages:edit-ingredient.component.ts was formatting procurement costs as en-US / USD; purchase-order-edit.component.ts was using Angular's CurrencyPipe with a hardcoded 'CAD' constant. Both are now replaced by a new TenantCurrencyService (Angular Signal + static constants DEFAULT_CURRENCY = 'CAD', DEFAULT_LOCALE = 'en-CA', DEFAULT_DIGITS_INFO = '1.2-2') and a new UmCurrencyPipe (| umCurrency: tenantCurrencyService.currencyCode()). en-CA locale registered in app.module.ts. Pipe returns null for null / undefined / '' / NaN; zero formats as $0.00. Currency code is an explicit pipe argument (not read from the Signal inside transform()) so Angular CD tracks it correctly. SaaS billing pages (enterprise-dashboard, promo-codes) intentionally retain USD and are not affected.
PO detail/edit consolidation (PR 4 of PO bug sweep, Bugs 2, 4, 7): Deleted the standalone PurchaseOrderDetailComponent and flipped the /inventory/purchase-orders/:id route to PurchaseOrderListComponent, which opens the existing edit modal in readonly mode. Readonly view now includes Vendor Code and Unit columns on the line-items table (populated from backend-denormalized vendorProductCode / packDescription fields). In Submitted / Partially Received views, the Received column sits immediately beside Order Qty for easy visual comparison. Overflow header actions (Download PDF / Download Excel / Print / Duplicate / Cancel) are consolidated into a single 3-dots menu matching the order-detail / recipe-detail idiom. Edit is a prominent footer button on draft POs. Preview PDF has been removed — Download PDF covers the use case. Catalog fetch is gated behind !readonly to skip unnecessary HTTP requests on view-only loads. The Received column and per-row line-item Status badge are hidden on draft POs. Clicking Mark as Sent without an expected delivery date fires a concise toast instead of blocking with a disabled button. The "Demi won't email your supplier" message is consolidated into a single tooltip on the Mark as Sent button. Download PDF and Download Excel use the existing async Cloud Tasks + GCS pipeline (exportBatch + polling) with a new single-file bypass in BatchExportService that skips ZIP overhead for single-PO exports. Pure PoLineItemStatusPipe added to replace the previous method-in-template anti-pattern for line-item status badges.
Cloud Tasks write path for AI insight feedback (PR 3 of 7) — every thumbs-up / thumbs-down vote on an AI insight now dispatches a Cloud Task to POST /internal/record-ai-feedback, which writes an append-only audit row to ai_insight_feedback with full point-in-time user + recipe + swap context. The dispatch is awaited with a 1500ms timeout and swallowed to Sentry on failure so the user vote always returns 204; at-least-once deliveries collapse onto the UNIQUE(idempotency_key) constraint from PR 2. Ships with an FF_FEEDBACK_CAPTURE kill-switch (dispatch AND worker both short-circuit) and a GDPR anonymizeUser stub. Requires gcloud tasks queues create ai-feedback-queue --location=northamerica-northeast1 --max-attempts=5 --max-backoff=300s --min-backoff=10s in each environment before first deploy. Unblocks PRs 4–7.
Cloud Scheduler retention purge for AI insight feedback (PR 7 of 7) — daily POST /internal/purge-ai-feedback endpoint deletes ai_insight_feedback rows older than FF_FEEDBACK_RETENTION_DAYS (default 365) and stale ai-feedback-export-*BatchJob rows older than FF_FEEDBACK_EXPORT_BATCH_JOB_RETENTION_DAYS (default 30). OIDC-authenticated (Cloud Run IAM + inline google-auth-library iss/aud/email verify), cross-tenant, idempotent, timezone-safe UTC math. Failures log at error level and re-throw so Cloud Scheduler retries per job policy. Requires GCP provisioning before first deploy — see .claude/context/DEPLOYMENT-GUIDE.md for the gcloud scheduler jobs create command.
ai_insight_feedback MySQL audit table for AI insight thumbs-up / thumbs-down votes — new AiInsightFeedback model + AiInsightFeedbackRepository landing the schema for the AI Insights hardening initiative. Append-only row per vote with denormalized point-in-time user / recipe / swap context (user email + display name, recipe id + name, source and target ingredient ids + names, signed cost_delta_cents, sentiment enum 'up' | 'down'). idempotency_key varchar(36) NOT NULL UNIQUE guarantees at-least-once-delivery safety for the Cloud Tasks worker that will land in the next PR — the write path will catch ER_DUP_ENTRY silently so retry storms produce exactly one row per vote. reason_chips json and comment text columns are reserved for a future expansion so the schema is forward-compatible without a follow-up ALTER TABLE. Five composite indexes cover the admin browser query patterns: primary (tenantId, created_on), sentiment-filtered (tenantId, sentiment, created_on), cross-tenant analytics (insight_type, created_on), and per-user audit / GDPR anonymization (user_id, created_on). A post-schema migration at backend/src/scripts/migrate-ai-insight-feedback.ts defensively re-applies the UNIQUE constraint and every composite index on every deploy (idempotent — re-running is a no-op when the indexes are already present) so drift from LoopBack's alter-mode migrateSchema can't leave the table unindexed. (PR 2 of 7 in the AI insights hardening initiative — schema-only, zero controller wiring.)
Changed
Staging MongoDB Atlas cluster migrated from AWS ca-central-1 to GCP northamerica-northeast1, eliminating the cross-cloud latency hop that staging Cloud Run was paying on every query; production cluster untouched. New cross-cloud Atlas region migration runbook added to docs/infrastructure/mongodb-atlas.md (referenced by P28 in the GCP cutover runbook) for the eventual prod migration.
Fixed
"Create PO" button no-op — GenericManagementService.processItemAction() switch had no case 'create' handler, so createOrder() fired itemAction({ action: 'create' }) and returned silently without opening the modal. Added case 'create': return this.addItem(); so the create flow is correctly routed. Found during QA of the PO modal refactor.
SweetAlert dialogs fail to render after a preConfirm dialog — defaultAlertOptions in alerts.component.ts was setting willOpen, didOpen, willClose, didClose, didRender, and didDestroy to undefined. The ngx-sweetalert2 SwalComponent exposes these as @Output() EventEmitters; the swalOptions setter calls Object.assign(this, options) which overwrote those EventEmitters with undefined, causing TypeError: Cannot read properties of undefined (reading 'emit') and preventing any subsequent popup from rendering. Removed these six keys from the defaults (they must never appear there) and added a JSDoc block explaining why.
HubSpot Founder Cockpit "Load failed" error from HubSpot 429 rate limiting — concurrent requests to /cockpit/api/metrics each fired independent HubSpot API calls in parallel, saturating the rate limit and causing the dashboard to show "Load failed". Fixed with in-flight Promise deduplication (cockpitMetricsFetchInFlight Map): concurrent requests for the same pipelineId now share a single in-progress fetch. Cache TTL increased from 60s to 300s (5 minutes) to further reduce HubSpot API call frequency. Error message in the dashboard now includes the specific error detail instead of the generic "Load failed — will retry".
HubSpot Sales OS control plane TypeScript errors — fixed 23 pre-existing TypeScript strict-mode violations in server.ts: added HubSpotApiError interface (extends Error with .data and .status fields), typed evaluateDealRisk and modelDealValue parameters as Record<string, unknown>, narrowed reason instanceof Error in unhandledRejection handler, cast rampSkipsByAction reduce accumulator, added union discriminant cast for validation .reason field. All tsc --noEmit errors resolved.
HubSpot card unit tests failing with jest is not defined — DemiSalesOsCard.test.tsx used jest.mock/jest.fn() APIs in a Vitest project. Replaced all jest.* calls with vi.* equivalents. Fixed temporal deadzone crash (Cannot access 'DemiSalesOsCard' before initialization) caused by the extend mock immediately invoking its callback on import before the component const was initialized — extend is now a no-op vi.fn().
evaluateDealRisk callers referencing non-existent .level and .reasons fields — two card action endpoints merged from main referenced riskEval.level and riskEval.reasons (plural array), neither of which exist on the return type. The function returns { risk, reason } (singular string). Both call sites corrected to use riskEval.risk and riskEval.reason.
PO list rows now navigate to detail on click + Cmd/Ctrl+Click opens new tab — <um-paginated-table>'s row-click behavior is now opt-in via a new [rowClickable] Input. Setting it to true adds the cursor-pointer affordance, emits rowClick with a { item, event } payload, and gates role="button" + tabindex="0" on actually-loaded rows so screen readers no longer announce skeleton placeholders as buttons. Critically, the previous behavior unconditionally wired every row in every table (44+ consumers) as a keyboard-focusable button with no handler, a latent WCAG violation that this PR fixes app-wide. Only ai-insights and the new PO list opt in. PO list viewOrder() repointed to navigate to the existing detail route so the row-click and 3-dots "View" action converge on the same destination. (PR 5 of the post-first-gen PO bug sweep, Bug 1.)
Editing an existing PO no longer fires the unsaved-changes guard on load — purchase-order-edit.component.ts previously enriched lineItems$ with stockStatus/priceChangePercent/etc. fields AFTER resetInitialState() had captured the dirty baseline; the differ saw those phantom mutations and flagged the form as dirty before the user touched anything. Display values are now computed via two new pure pipes (StockStatusPipe, PriceChangePipe) in the template, so lineItems$ stays immutable after load. The vendor-catalog lookup maps (stockMap, parMap, trackedSet, lastPoCostMap) are now reassigned to fresh instances on every catalog load instead of being mutated in place, satisfying the pure-pipe reference-stability contract. (PR 5, Bug 3.)
SweetAlert callback leak between sequential dialogs — preConfirm, preDeny, inputValidator, willOpen, didOpen, willClose, didClose, didRender, and didDestroy callbacks no longer leak from one alert into the next. ngx-sweetalert2's SwalComponent retains every touched Input via an internal touchedProps Set; an Input that's missing from the next caller's options is silently re-emitted with its previous value. The shared defaultAlertOptions map now explicitly resets every callback / lifecycle Input with undefined, with a JSDoc rule documenting the convention so future engineers don't reintroduce the leak. (PR 5, Bug 6.)
PO detail "Submit Order" button renamed to "Mark as Sent" with confirmation modal — clarifies that Demi does not email the supplier directly. New helper text under the button (Demi won't email your supplier), and a confirmation modal with the copy "This flags PO-{N} as sent to {vendor} so it counts toward inventory tracking. You still need to email or call your supplier separately. Demi does not send the order automatically." The modal uses SweetAlert's preConfirm + showLoaderOnConfirm so the loading spinner stays visible during the submit network call (prevents double-submit) and allowOutsideClick: false so users can't dismiss mid-flight after the backend has already committed. Success toast: "Marked as sent. Remember to send the PO to {vendor} directly." (PR 5, Bug 8.)
PO list "Delete" action returned 404 "API not found !" — the row-action menu on /inventory/purchase-orders was calling DELETE /api/purchase-orders/{id}, which the backend deliberately does not expose (purchase orders are cancellable, not deletable, to preserve the audit trail for regulatory compliance and vendor-invoice reconciliation). Renamed the action to Cancel (ban icon, still styled text-danger) and rewired cancelOrder() to POST /purchase-orders/{id}/cancel — the same endpoint the controller already ships. Confirmation modal copy is deliberately minimal: *"PO-XXXXX will be cancelled. This action cannot be undone."* Pre-existing bug — surfaced on 2026-04-08 during the manual Step 0 cleanup for PR #1165 when attempting to remove the 4 bad-quantity draft POs on Vancouver staging; not caused by PR #1165 (git diff f5619a943..b9ab4d160 only touched one test-fixture file in the PO service test suite). The PO edit modal also gains a matching 3-dots dropdown in the header (mirrors the edit-order.component.html dots-horizontal pattern) with a single Cancel purchase order item gated to existing drafts — operators can now cancel a draft PO from inside the open modal without closing it first, and on success the modal emits saved({exit: true}) so the parent list reloads and the modal closes in one pass. Both entry points share byte-identical title / body / button copy and call the same poApi.cancel(poId) endpoint. Added 12 new unit tests total — 4 on the list (modal copy, happy path, failure branch, tableActions shape) and 8 on the edit modal (canCancelOrder gating across 4 PO states, modal copy, no-op on unsaved PO, happy-path emits saved(exit:true), failure branch keeps the modal open).
Changed
Flattened nested subscribe in purchase-order-edit.loadVendorCatalog — pre-existing getWithLineItems subscribe nested inside the forkJoinnext callback is now flattened with switchMap, with explicit catchError handling that falls back to an empty lastPoCostMap if the last-PO lookup fails. The catalog itself is still usable without the price-change badges, the UI never hangs on a half-loaded state, and the observable lifecycle is managed in one chain. (PR 5 cleanup, surfaced by Gemini CTO review.)
PO auto-generate soft-degrades instead of halting when some ingredients have bad procurement data (Bug 5 — PR 2 of the PO bug sweep) — computeDeficitsWithProcurement() now collects ingredients with invalid procurement into an invalidDeficits list instead of halting the entire run. Generation continues with the remaining valid ingredients; the operator sees a persistent warning toast naming the excluded rows (up to 5 inline, "+N more" suffix) and can fix them in Inventory Management before regenerating. A complete halt is still triggered when ALL deficit ingredients are invalid — there is nothing left to generate. New 'unit-conversion-failed' reason code surfaces the case where a recipe uses one measurement class (e.g. grams) while the inventory unit is in an incompatible class (e.g. litres); silently using that demand would inflate the order quantity with values in the wrong unit. getQtyPerCase in the unit-conversion utility now distinguishes null/undefined (2-tier procurement — treat as 1) from explicit 0 or negative (corrupted data — return null so callers surface it instead of silently dividing by zero). FLOAT_TOLERANCE = 1e-9 constant applied to both Math.ceil(suggestedCases - FLOAT_TOLERANCE) call sites so IEEE 754 float division (e.g. 100 / 25 = 4.0000000000001) cannot trigger an off-by-one extra case order. Math.max(0, Math.ceil(...)) added as a guard against the degenerate suggestedCases < FLOAT_TOLERANCE case. Frontend warning toast message is intentionally generic (not "incompatible unit configurations") because the invalidDeficits array can contain any InvalidDeficitReason (e.g. invalid-case-qty, null-shelf-unit) depending on what the procurement gate caught. Backend unit-conversion utility gains full unit-test coverage: FLOAT_TOLERANCE value + float-imprecision guard, all four conversion functions with 2-tier/3-tier/null/zero/negative inputs. (PR 2 of the post-first-gen PO bug sweep.)
Inventory tracking wizard now blocks corrupt procurement data upstream — extended the shared validateProcurement() validator with 4 new structural checks (invalid-case-qty, null-shelf-unit, invalid-shelf-amount, missing-conversion-weight) covering the cucumber/parsley failure mode that produced 471 cases of parsley ($16.6K line) on Vancouver staging on 2026-04-08. The wizard's IngredientInventoryConfigController.batchUpdate now runs the full 8-check validator before opening any transaction; if ANY ingredient in the batch fails, the entire request is rejected atomically with a structured 422 Unprocessable Entity response carrying details: {code: 'TRACKING_VALIDATION_FAILED', failures: [{ingredientId, ingredientName, reasons}]}. The frontend wizard surfaces each failure inline with a "Fix this row" deep-link to the ingredient editor and an "Unselect" recovery button, blocking the "Try again" button until every bad row is resolved. New read-only GET /ingredient-inventory-config/audit-tracking-readiness endpoint runs the same validator across the calling tenant's currently-tracked ingredients and returns the list that would now fail the gate, so operators can preview the impact and clean up legacy data BEFORE the corresponding PO auto-generate soft-degrade ships. The audit endpoint resolves the effective inventoryUnitId via the same fallback chain batchUpdate uses, so legacy rows with inventoryUnitId = NULL in the DB still trip the missing-conversion-weight check instead of being falsely reported as healthy. Cloud Logging strips ingredientName from validation failure metadata to avoid leaking proprietary recipe terms across tenants — the structured 422 still carries names to the frontend so the operator sees them in the wizard. Backend gains 26 new unit tests covering each new failure mode, priority ordering, the cross-unit-class skip path, the wizard's atomic-rejection guarantee, and the audit endpoint's effective-inventoryUnitId resolution. Verified by Gemini 3.1 Pro CTO review (Vertex AI) before merge — caught 1 critical (audit-endpoint NULL-inventoryUnitId bypass) + 1 high (PII in logs). (PR 1 of the post-first-gen PO bug sweep.)
PO auto-generate failed with EntityNotFound for every vendor on GCP staging — PurchaseOrderService.createDraft() opens a per-vendor MySQL transaction at READ_COMMITTED isolation, creates the PO row inside the transaction, then immediately calls recalculateSubtotal → poRepo.updateById(..., txOptions). The audit-wrapped updateById (and the matching update / replaceById / deleteById / deleteByIdHard overrides) was doing its pre-fetch via findByIdIncludeSoftDelete(id) *without forwarding options*, so the read checked out a fresh connection from the pool that ran outside the transaction. Under READ_COMMITTED, the just-created PO row was invisible to that out-of-band read, the audit pre-fetch threw EntityNotFound, the transaction rolled back, and the per-vendor catch in createFromBelowPar flipped that vendor into failedVendors. Every vendor failed identically — the operator-facing toast read "Created 0 purchase order(s), but N vendor(s) failed". Fix: forward options to all 5 findByIdIncludeSoftDelete(id, options) call sites in default-auditable-entity.repository.ts so the audit pre-fetch joins the caller's transaction and sees the in-flight row. This is also a latent correctness improvement for any future "update twice in one transaction" path — the audit log was previously reading pre-transaction state and missing intermediate uncommitted changes. Verified by Gemini 3.1 Pro CTO review (Vertex AI) before merge.
Wizard procurement integrity helper extracted from PR #1154 — the 4-check classification loop in PurchaseOrderService.computeDeficitsWithProcurement is now a shared helper at backend/src/services/shared/procurement-integrity.ts (validateProcurement + isValidCost) so the upcoming inventory tracking wizard gate can call the same validator without drift. Behavior-preserving refactor — PO generate still routes no-procurement to the soft-skip path and the other 3 reasons to the fail-loud halt; existing 47 PO service tests are unchanged.
PO auto-generate still returned calculation-failed post-PR #1150 — root cause was two classes of broken procurement data in staging: (1) IngredientProcurement rows with NULL purchaseUnitId that passed the old proc && proc.vendorId filter, dropped null into the PurchaseOrderLineItem payload, and failed LB4 model validation inside the transaction; (2) vendors in an inconsistent soft-delete state (deleted=0 AND deleted_on IS NOT NULL) that vendorRepo.findById filtered out, causing EntityNotFound inside createDraft(). computeDeficitsWithProcurement() now pre-validates every deficit ingredient's procurement row — preloading all referenced vendors in one tenant-scoped vendorRepo.find and classifying each row against three hard integrity checks (null-purchase-unit, invalid-cost, unreadable-vendor). The cost check is NaN-safe (!(Number(x) > 0)) so undefined/null costs are rejected instead of bypassed. Deficits missing a default vendor entirely are soft-skipped (not halted) so one unconfigured ingredient cannot paralyze an entire facility's supply chain — the legacy MissingProcurement reason still fires when *every* deficit lacks procurement. If any HARD integrity failure is present, generation is halted entirely (no partial success — a silently dropped "tomatoes" ships kitchens short) and a new InvalidProcurementData reason code is returned with an invalidDeficits list naming every broken ingredient; the frontend shows a persistent (no auto-dismiss) warning toast listing the first 5 names semicolon-separated (so names like "Spice, Sumac" remain readable) with a "+N more" suffix, so the operator can fix every bad row in one pass and re-run. A new PartialSuccess reason code surfaces the case where some vendor POs were created but others threw unexpected runtime errors mid-loop — the frontend warns the operator by naming each failed vendor (so they can retry selectively) instead of silently dropping them. Per-vendor createDraft catch logs now include tenant + ingredient context AND the full stack trace so GCP Error Reporting can group by root cause.
Inventory Print / Export PDF returning 500 on staging — PDF service image was built before the /generate-inventory-pdf route landed, causing 404 Cannot POST wrapped as 500. Rebuilt from main. Backend now returns 503 ServiceUnavailable with a user-friendly message when the PDF microservice fails (matching the sibling 503 branch and the frontend's existing 503 handler). PDF service /health now returns the build commit SHA for one-curl version-drift diagnosis, mounted routes are introspected on boot via express-list-endpoints and logged as structured JSON, and cloudbuild.yaml region is no longer hardcoded to the wrong value. Same 503 + structured-logging pattern applied to the sibling PhysicalCountExportService and PurchaseOrderExportService for consistency.
Generate Purchase Orders always returned empty on staging — root cause was deliveryDate: {inq: [UTC-midnight Date, ...]} in the auto-generate flow, which never matched MySQL DATETIME rows that had any time component (a Vancouver order delivered 3 PM PDT is stored as 2026-04-12 22:00:00 UTC, not 2026-04-12 00:00:00). Replaced with a timezone-correct between range computed in the sub-tenant's local timezone, dropped the Execution-production-day DOW intersection so every calendar day in the window contributes, chunked the order fetch (500/page with setImmediate yield) to prevent Cloud Run OOM at scale, and applied symmetric defense-in-depth tenant scoping to every downstream repo call. KPL override now compares in the sub-tenant local day (same root-cause class the main query had). Demand calculation is wrapped in try/catch with structured error logging and a new CalculationFailed reason code surfaced as a red error toast, so Cloud SQL hiccups no longer masquerade as a success state. Auto-generate now returns a typed response with a specific reason code (Success, NoTrackedConfigs, NoOrdersInWindow, NoTrackedIngredientDemand, NoDeficit, MissingProcurement, AllAlreadyInOpenPos, CalculationFailed), each mapped to a severity-correct toast explaining exactly what the user needs to do next.
Inventory export returning 403 Forbidden — stock levels PDF and Excel exports now reach the correct sub-tenant. The export call was building HTTP headers manually and omitted x-tenant-id, x-sub-tenant-id, x-tenant-subdomain, and Accept-Language, so the backend resolved to the wrong sub-tenant and rejected the request. Now uses the shared ApiService.getTenantHeaders() helper so raw blob calls carry the full tenant context like every other API call.
AI Usage CSV export missing icon — dropdown referenced non-existent file-03.svg; now uses file-download-03.svg.
AI Usage PDF export was a raw print screen — replaced window.print() with proper jsPDF-generated PDF featuring Demi branding, KPI summary cards, styled data tables with teal headers, page-break logic to prevent row truncation, and a confidential footer.
AI Usage Excel export had zero formatting — applied the same branded styling used in Recipe/Ingredient exports: title section with Demi branding, blue column headers, bordered data rows, currency formatting, and period footer across all three sheets.
AI Usage CSV not machine-readable — restructured CSV for AI/ML processing with # comment metadata, snake_case headers, raw numeric values (no $ or commas), and three flat RFC 4180 sections.
PAR unit dropdown missing scaled units — Litre (for volume) and Kilogram (for weight) now appear in PAR unit dropdowns across the inventory dashboard bulk edit, ingredient detail modal, and setup wizard. Fixed measurement base unit passed as string instead of proper object in two of three callers.
PAR unit auto-conversion — switching between compatible measurement units (e.g., ml↔L, g↔kg) now auto-converts the numeric value instantly. Switching to incompatible units (e.g., ml→Bottle) resets to empty for manual entry.
Ingredient detail: current inventory field — tracked ingredients now show actual on-hand stock below PAR level. Untracked ingredients show a "Set up tracking" CTA that opens the wizard at step 2 with the ingredient pre-selected.
"Add to inventory tracking" link color — changed from Bootstrap blue to Demi brand teal.
PO auto-generate "sufficiently stocked" false positive — replaced vendor-level PO dedup (skipped ALL items for a vendor with any draft PO) with ingredient-level filtering, matching the reorder recommendations logic.
Export error handling — PDF export now returns 503 with "use Excel" message when PDF service is unavailable. Frontend shows status-specific error messages (403 permission, 503 unavailable, 422 validation).
Proactive insights cron job Redis crash — the daily insight warm-up cron was processing ALL active tenants with no rate limiting, overwhelming Redis on staging (641s run, cascading timeouts). Now filters to only Growth/Trial subscribers with AI Insights access, uses p-limit(1) sequential processing with 30s per-recipe timeout, 5min global job timeout, and per-tenant recipe caps. Removed uncontrolled fire-and-forget Tier 2 bulk warming entirely.
AI insights listing page hangs on Redis saturation — added 3-second timeout on Redis bulk read (mget). When Redis is slow or down, the page returns partial results with an info banner instead of hanging indefinitely.
SIGTERM graceful abort for Cloud Run — cron job now checks for SIGTERM between recipes and aborts gracefully with partial results logged, preventing hard termination during Cloud Run scale-in.
Added
Founder Cockpit — Prospect Funnel widget + saved HubSpot contact lists. New fourth row on /founder-cockpit renders every outreach stage (new / researched / day_0_drafted / day_0 / day_4 / day_9 / day_12 / day_16 / replied / demo_booked) with a horizontal bar, contact count, and deep link to the matching HubSpot saved list. Adjacent "Outreach KPIs" card surfaces total active, due today, tier 1 / tier 2 counts, replies, demos booked, reply rate, demo rate, and reply→demo conversion — all wired to the existing prospectFunnel payload from the control plane (frontend-only change; no backend edits). New scripts/hubspot-sales-os/provision-day-0-drafts-list.mjs idempotently provisions three DYNAMIC contact lists in the portal via the HubSpot Lists v3 API: "Day 0 Drafts — Ready to Send" (listId 77), "Sequence Enrolled — Day 4, 9, 16" (listId 78), "Replies — Needs Triage" (listId 79). Drew can now batch-review the 5 drafted cold emails from a single cockpit widget (click "Day 0 — Drafted" → HubSpot saved list) instead of clicking through contacts individually. Spec coverage added for the new funnel mapper, enrichment pct math, and the empty-funnel fallback.
HubSpot Sales OS — Day-0 send-detection scanner + day_0_drafted intermediate state. Closes the send-vs-draft timing gap in the Sequences-wins architecture. Previously send_day_0_and_enroll control-plane action set demi_outreach_stage=day_0 at DRAFT creation, which — when paired with the Enrollem enrollment workflow — scheduled the Sequence's Day-4 follow-up 3 business days from draft creation instead of 3 business days from actual send (so the Day-4 bump could fire before Drew had even sent the cold email). New flow: control plane sets day_0_drafted at DRAFT creation and pins the engagement ID on the contact via the new demi_outreach_draft_engagement_id property. New scanner POST /internal/scans/day-0-send-detection (added to tools/hubspot-sales-os-control-plane/server.ts) polls contacts in day_0_drafted (within a 7-day freshness window), reads the pinned draft engagement directly, and flips stage to day_0 once hs_email_status=SENT and hs_email_direction=EMAIL. Engagement-ID gating eliminates the false-positive on same-day historical outbound emails and removes the need for association pagination or timestamp heuristics. The stage flip triggers HubSpot workflow 3990922202 → Enrollem "Enroll Contact in Sequence" → Demi Founder Cadence. Cloud Scheduler job hubspot-day-0-send-detection (every 5 min, 07:00–19:59 Mon–Fri America/Vancouver) added to scripts/hubspot-sales-os/setup-cloud-scheduler.mjs — max ~5-min latency from Send click to sequence enrollment. Scanner hardening: in-flight lock short-circuits overlapping Cloud Scheduler runs with 429, bounded concurrency of 3 + per-contact try/catch isolates single-contact failures, and a 7-day demi_outreach_start_date filter prevents stuck-contact reprocessing. New day_0_drafted option added to the demi_outreach_stage HubSpot enum at displayOrder 3; new demi_outreach_draft_engagement_id string property added (via HubSpot Properties API). No migration of existing contacts needed.
Backend exports redesign — Phase C (PDF microservice templates + logo data-URI inlining) — rewrites all four Handlebars templates in services/demi-pdf-service/ (purchase-order.hbs, physical-count-report.hbs, physical-count-sheet.hbs, inventory-stock-list.hbs) to match the locked "After" mockup. Shared design tokens (brand #089374, text hierarchy, surface / border / pill palettes), tabular-nums on numeric columns, page-break discipline (tr, .kpi-row, .notes-block, .totals-block all get page-break-inside: avoid), thead { display: table-header-group } for multi-page header repeat, and @page { margin: 16mm 18mm; size: Letter }. Status pills, KPI cards, stacked item + SKU / category / vendor cells, totals block without the heavy 2px rule, notes block with gray surface (no yellow warning). Shared types.ts rewritten to match the new backend payload (currency, vendor.address as structured AddressLike, shipTo.attn, lineItems[].uomLabel, docId on PC data, locationName on Inventory tenant, statusTimeline dropped from PO). Routes pre-compute status-pill class, meta-column class, per-item subtitles, and variance classes server-side so the templates stay simple (no nested Handlebars ifs, no eq helper). The currency helper was renamed from currency to fmtMoney (Gemini CRITICAL #1) to avoid shadowing the currency data field — {{currency}} now safely resolves to the code; {{fmtMoney value currency}} formats money. data.items.map(...) and data.lineItems are now defensive against null / non-array (Gemini HIGH #2). The empty meta block now renders nothing at all (Gemini MEDIUM #5) via a server-computed hasMeta flag. The backend generatePdf() on the PO and Physical Count export services now converts tenant.logoUrl to a data: URI via the shared fetchLogoAsDataUri() helper before POSTing (Gemini MEDIUM #4) — Inventory already did this, but PO and PC were leaking raw HTTPS URLs that Puppeteer's setRequestInterception SSRF guard would abort, rendering as broken-image icons. 159 existing backend unit tests still green. Gemini 3.1 Pro pre-merge review: round 1 found 5 real issues (1 CRITICAL, 2 HIGH, 2 MEDIUM) all fixed; round 2 verdict GO.
Backend exports redesign — Phase B (frontend currency selector + receiving contact input) — PO edit modal now has a Currency dropdown (CAD / USD) below the Expected Delivery Date field; backend default still applies on create (tenant country → CAD/USD) but users can override per PO for cross-border purchases. Location detail page (/saas-management/settings/locations/:id) now has a Receiving contact for POs text input in the Location preferences section; the value populates the "Attn:" line on PO ship-to blocks when set, and is omitted entirely otherwise. Both fields persist through existing save paths — PurchaseOrder.currency is a column added in Phase A, receivingContactName lives inside SubTenantConfiguration (JSON). Frontend PurchaseOrder and SubTenantConfiguration TypeScript models extended to match.
Backend exports redesign — Phase A (shared helpers + Excel rewrites + field mapping) — rewrites the Excel generators for Purchase Orders, Physical Counts (Sheet + Report variants), and Ingredient Inventory to match the locked "After" mockup at /tmp/demi-mockups/exports-design-system.html. Excel outputs now get document-specific sheet names (no more Sheet1), frozen column header rows, dynamic AutoFilter ranges, real pageSetup with printTitlesRow, and status pills rendered as cell fills (not colored text). Two new shared helper modules: backend/src/services/helpers/export-format.helper.ts (formatDate / formatAddress / formatPhone / formatCurrency / getTenantHeader — address formatting canonicalizes country codes and rejects empty segments; phone strips the legacy CA- / US- prefix concatenation bug) and backend/src/services/helpers/excel-style.helper.ts (getStatusFill with ARGB palette, applyHeaderBlock, applyPrintSetup, setColumnHeader, freezeHeaderRow). fetchLogoAsDataUri extracted to logo-fetcher.helper.ts with an added fetchLogoAsBuffer variant for ExcelJS workbook.addImage embedding (HTTPS SSRF guard preserved). Field-mapping gaps closed: PurchaseOrder.currency field added (derived from tenant.configuration.address.country at create — US → USD, else CAD), SubTenantConfiguration.receivingContactName added (surfaces as "Attn:" on PO ship-to block when set, omitted otherwise), PO export service now loads vendor address via include: [{relation: 'address'}] and populates uomLabel on each line item via a batch UnitRepository.find({where: {id: {inq: [...]}}}) lookup, Ship To name now correctly assembled as ${tenant.name} — ${subTenant.name} (was previously dropping the tenant and showing only subtenant). Empty fields are now hidden entirely in the meta grid / KPI summary — no em-dash placeholders. Internal statusTimeline dropped from vendor-facing POExportData payload (was leaking audit trail onto the vendor doc). Gemini 3.1 Pro pre-merge review surfaced two real findings, both fixed: safeText regex now catches leading whitespace (/^\s*[=+\-@]/) so " =1+1" can't bypass the formula-injection guard; new logo-fetcher.helper.test.ts locks in 13 assertions on the SSRF guard + content-type → extension mapping. PO Excel drops the Shipping totals row (field isn't captured by the UI); Inventory drops the "Currency" meta cell (internal doc, not needed); Physical Count status pill moved to dedicated row 4 F4:G4 (was crowding the meta kickers in column 2). PDF generators still point at the external PDF microservice — PDF HTML template rewrites ship in a follow-up Phase C (separate repo); only Excel + payload shape ship in this PR. New backend/src/types/cacheable-lookup.d.ts is a minimal type stub for the untyped cacheable-lookup@6.1.0 dep introduced by #1237 (project tests run on compiled JS, so the missing declaration was silently masked until tsc --noEmit surfaced it during this work). 159 passing unit tests (146 baseline + 13 new logo-fetcher assertions).
Inventory pre-production hardening phase 3 — rounding at persistence boundaries + Typesense sync on depletion (#1243). New backend/src/utils/decimal-rounding.util.ts (roundForPersist, sumForPersist) applied at every ledger write (depletion, goods receipt, on-hand sum) so accumulated IEEE-754 drift can't reach DECIMAL(16,8) columns. MySQL was truncating unrounded floats producing UI-vs-ledger divergence on bulk receipts. Post-commit ingredientRepo.syncToTypeSense() now fires on depletion success (outside the tx try/catch — a sync failure can't rollback a committed ledger). 10 unit tests lock the drift-free contract.
Inventory pre-production hardening phase 4 — PO compare-and-swap locks + bulk-receive version fix + permission matrix (#1244). cancel(), bulkSubmit(), bulkCancel() on purchase orders now use updateAll({status: X}, {id, status: {inq: [...]}}) with bypassCache: true so concurrent status transitions fail with 409 Conflict (status race, not 400 BadRequest). bulkMarkAsReceived now captures the bumped version from updateReceiptItems before calling confirmReceipt — every multi-item bulk-receive previously 409'd on OCC because the stale receipt.version was passed through. Canonical permission matrix at docs/inventory-permission-matrix.md documents every inventory endpoint → @authorize mapping (audit result: clean across 5 controllers).
Inventory pre-production hardening phase 5 — frontend error handlers + wizard dirty guard + UI polish (#1245). stock-history.component.ts load failures now log, toast, and navigate back instead of silently stranding the operator on an empty view. receiving-list.component.ts and goods-receiving-route-wrapper.component.ts replace non-null-asserted onHidden! with safe onHidden?.pipe(...) so a rapid dismiss-before-subscribe can't blow up. Inventory-setup wizard close() prompts via AlertsService when dirty (selection changes or non-zero stock entered on new items); Number.isFinite guard in executeConfirmAndSave() bounces invalid stock back to step 2 with an actionable inline error instead of letting the backend reject with 400. Dashboard recommendations *ngFor gained trackBy: trackByVendorId to prevent DOM thrash when toggling a single vendor card. UI polish: dropped the colored left-border accent on .quick-action-banner, replaced the custom .table-empty div on PO-create with <um-empty-state>, swapped <i class="fa fa-redo"> for <fa-icon>.
Inventory pre-production hardening phase 6 — vendor XSS strip + field maxLength + E2E regression guards (#1246). New backend/src/utils/sanitize-vendor-strings.util.ts strips HTML tags from vendor free-text fields (name, mainContactName, notes, deliveryInstructions, accountNumber, paymentTerms, url, image). Two-pass (strip tags → decode </>/</< entities → strip again) closes the unterminated-tag and numeric-encoded-tag bypasses that a naive <[^>]*> regex misses. Applied at the top of VendorController create / updateAll / updateById / replaceById. maxLength constraints added to previously unbounded fields (name 255, mainContactName 255, url 500, image 500, notes 5000). E2E guards in tests/e2e/inventory/inventory-management.spec.ts cover the wizard dirty-close prompt (PR-16) and cross-tenant IDOR handling (phase 1).
Inventory reorder recommendations now open the modal Create PO — clicking "Start PO" from the dashboard's reorder-recommendations panel opens the modal create-PO experience with the vendor and suggested line items pre-populated (same path as + Create PO), instead of dumping the user into the legacy full-screen form.
Client-side inventory print — the dashboard's Print action renders a local print view (InventoryPrintService + PrintableInventoryComponent) instead of round-tripping a PDF from the external service. Opens the browser print dialog in under a second vs. the prior ~15-20s wait, matching the existing invoice-print pattern. Backend exposes ?format=json on /ingredient-inventory-config/export for the payload.
Changed
Founder Cockpit — Revenue Forecast chart legend cleaned up; integration + feature docs refreshed to match shipped proxy architecture. The cockpit's forecast chart dataset was reduced to the probability-weighted series only in an earlier revision (upstream metrics endpoint returns a single totalAmount per month), but the HTML legend and subtitle still advertised a second "Best case" dashed line that was never rendered. Removed the orphan legend item and updated the subtitle to "Probability-weighted pipeline by close month" so the UI no longer lies about what's on the chart. Rewrote docs/integrations/hubspot-sales-os.md from the retired direct-to-control-plane architecture (GET /hubspot/cards/founder-cockpit/dashboard, no auth) to the shipped LoopBack-proxy reality (GET /api/cockpit/metrics → CockpitController → control plane with server-side Bearer token, session JWT + ManagePlatformSettings on the proxy, error-code taxonomy). Refreshed docs/features/founder-cockpit.md data-flow diagram (direct-call diagram → proxied flow with shareReplay(1)'d metrics$) and removed the stale STUB_DASHBOARD reference (service now returns emptyDashboard() on error, no mockup fallback ships).
ProductionDaysRecord.depletionStatus now reports 'partial' on Typesense-sync-only failures — the enum value existed but was never set. IngredientDepletionService.calculateAndRecordDepletion now returns a typesenseSyncFailed flag; production-data.service.ts sets depletionStatus = 'partial' when the ledger commits but one or more post-commit syncToTypeSense() calls reject. Ledger count is correct but search results may lag until sync is retried. Ops distinguishes this from 'failed' (ledger itself did not commit) via the dashboard badge.
Inventory PDF/Excel exports align with the dashboard's 5-band status logic — "Out of stock" (0 on hand), "Critical" (<50% PAR), "Below PAR", "In stock", "No PAR". Below-PAR summary count now includes out-of-stock items, matching the dashboard KPI. PAR column displays primary units only (secondary line removed). Backend export now treats parLevel === 0 as "In stock" (explicitly "no minimum needed"), matching the repository + frontend logic.
Expected Delivery Date is required when creating a PO manually — the Create PO modal and reorder Start PO flow now surface the * required indicator and the form blocks save until a date is chosen. Editing existing POs (including auto-generated drafts created without a date) is unaffected.
Received metadata split on received POs — the readonly PO view now shows "Received by {name}" and "Received on {full timestamp}" on separate rows to match the inventory print layout.
Optimistic bulk-cancel on the Purchase Orders list — bulk-cancelling draft/submitted POs flips each row's status badge to "Cancelled" immediately on API success, instead of waiting for a full reload.
Purchase Orders refresh button shows progress — the header refresh icon now spins and disables while a manual refresh is in flight, with a safety timeout that clears the spinner if the underlying list never emits.
Vendor → PO integration surfaced end-to-end (#1248) — selecting a vendor on the Create PO modal now auto-populates the Notes textarea from the vendor's "Default PO note" (vendor.deliveryInstructions). Preserves custom edits: notes only swap when the previous value still matches the prior vendor's default, tracked via _lastAppliedVendorNoteDefault. Readonly PO view now surfaces orderingEmail (as an "Orders" row), the vendor's structured street address, and renames "Submitted"/"Submitted by" → "Placed on"/"Placed by" (matching the existing Received on/Received by pattern, formatted as medium for consistency). The inline vendor caption below the Create-PO vendor select prefers orderingEmail over the rep email when both are set, since ordering email is the address POs will actually be sent to. Ingredient-name cell in line items dropped the ↑/↓ N% price-delta indicator — kept on the inventory dashboard, removed from PO context where it was noise.
Credit terms: removed Net 8 from the canonical CREDIT_TERMS constant — not a standard payment term.
Added
Vendor screen overhaul — rebuilt the Add/Edit Vendor modal on Demi's current settings-section layout (matches the edit-customer pattern). The modal now surfaces fields that already existed on the backend but were hidden in the UI, plus a new structured address:
Structured address via the global <um-address-input> (Google Places autocomplete) — persisted through a new addressId FK on Vendor and a shared Address row, mirroring the customer pattern. Vendor controller overrides create / updateById / replaceById to create-or-update the Address row and set addressId atomically. Migration migrate-vendor-address-and-index.ts adds a composite (tenantId, subTenantId, accountNumber) index and an addressId index online (ALGORITHM=INPLACE, LOCK=NONE).
Account #, Rep name (= mainContactName), Rep email (= email), Ordering email (tooltipped), Credit terms (ng-select backed by the new shared CREDIT_TERMS constant, with [addTag] so legacy DB values like Net 45 render and a trim+dedupe callback prevents duplicate tags), and Default PO note (= deliveryInstructions, tooltipped, maxlength="500") — all with aria-invalid bindings and markAsTouched on save so required-field errors light up on the first click.
Vendor list now includes an Account # column (sortable, backed by the new composite index).
Logo uploader switched from the legacy <um-upload-image> to the shared <um-image-uploader-row> component (matches customers / ingredients).
PO export DTO extended with orderingEmail, accountNumber, and vendor address so downstream PDF/XLSX templates can render them.
Ingredient importer populates vendor fields opportunistically — VendorInfo DTO extended with optional vendor_email, vendor_phone, vendor_account_number, vendor_payment_terms, vendor_ordering_email, vendor_postal_code, vendor_country_division, vendor_address2. When the upstream AI extractor supplies any of them, the importer stamps them onto newly-created vendors and best-effort fills empty columns on existing vendors (never overwrites user-populated fields). Email values are validated via the shared validateEmail helper; invalid values are dropped with a warning log. Extracted addresses now create a structured Address row instead of being stuffed into the free-text notes field.
VendorRepository write-path normalization — overrides create / createAll / updateById / updateAll / replaceById to collapse empty-string email / orderingEmail / accountNumber / phone / paymentTerms to null, so the PO send fallback (orderingEmail || email) can't land on "" and PATCH can explicitly clear a column.
Shared CREDIT_TERMS constant — mirrored in frontend/src/app/shared/constants/credit-terms.constant.ts and backend/src/constants/credit-terms.constant.ts, consumed by both edit-customer (refactored off its inline literal), the new edit-vendor, and the ingredient importer's normalizePaymentTerms helper so canonical values don't drift across the stack.
Vendor address lifecycle hardening — vendor controller uses delete-first-then-update semantics when a client clears the address (prevents orphans if the Vendor update fails); orphan-cleanup failures on create / clear / replace are escalated to Sentry with tags: { context: 'vendor-address', kind, vendorId, addressId } per the project Sentry convention. Address payload on create now requires address1 + city minimum (aligned with importer strictness) — partial country-only or postal-only payloads are rejected with HTTP 400 instead of polluting the Address table.
Ingredients bulk editor: explicit save/discard pattern — replaced auto-save-on-debounce with a Save/Discard banner (matching the recipes bulk editor UX). A dirty-state service (IngredientBulkEditorChangesService) tracks which rows have changed and emits a reactive count. Changes are batched and saved sequentially; partial failures leave failed rows dirty so users can fix and retry without losing context. Navigation away with unsaved changes triggers a confirmation dialog via the existing UnsavedChangesGuard.
Ingredients bulk editor: Par stock and Track inventory columns — IngredientInventoryConfig records (separate API model) are batch-fetched after each page load and merged into the row data. Par stock accepts a number input; Track inventory uses a toggle switch. Both fields participate in dirty tracking and are saved (or upserted if no config exists yet) alongside the ingredient on batch save. A skeleton/spinner is shown in those cells while configs are loading.
Ingredients bulk editor: 3-state status selector — replaced the Active/Inactive toggle with um-status-selector [includeDraft]="true" so Draft status is now selectable directly in the bulk editor.
Ingredients bulk editor: Label name moved to Procurement group — Label name column relocated from the Core attribute group to the rightmost position in the Procurement group, matching the edit-recipe form layout.
Search and filters in bulk mode (Ingredients) — when Typesense search is active, the Typesense table is kept alive via [hidden] (instead of destroyed via *ngIf) so its data feed continues in bulk mode. A dedicated filter strip (search input, Status, Ingredient Categories dropdowns, Reset button) is shown above the bulk editor. Filter controls lock (opacity + pointer-events: none) when unsaved changes are present to prevent silent data loss from a page re-fetch.
Search and filters in bulk mode (Sub-Recipes / Products) — the existing filter bar (Status, Recipe Categories, Workflows) is already rendered outside the list/bulk mode guard, so it remains visible and functional in bulk mode. Filter controls lock identically when unsaved changes are present.
Products (Meals) bulk editor: Default retail price and Default wholesale price columns — two number inputs (step 0.01, min 0) appear in a Pricing column group only when recipeTypeName === 'Meal'. Both fields are tracked for changes, restored on Discard, and included in the batch save payload.
Fixed
Exports polish v3 — PO payment-terms snapshot fallback + collapsed delivery block + Location meta de-duped on Inventory. Three fixes spanning backend export services and the PDF microservice templates:
**PO Payment Terms / Vendor Account fall back to the vendor when the PO snapshot is null *or* empty string.** po.paymentTerms and po.vendorAccountNumber are captured at create time; if the vendor's terms/account are filled in after the PO was drafted, the snapshot stays null and the export would render nothing. Export service now does po.paymentTerms || vendor.paymentTerms || undefined (same for account number) so a newly-configured vendor field flows through to in-flight POs. Empty-string sentinel (user cleared the field intentionally) also falls through to the live vendor value — matching the || null guard elsewhere in the service so the two fallback chains stay consistent. Four new unit tests lock the frozen-snapshot / fallback / empty-string / account-number cases.
PO Delivery Date collapsed into the Delivery notes block. The meta grid previously rendered a standalone "Delivery Date" cell as a full-width bar whenever only that field was set (Payment Terms + Vendor Account empty) — visually awkward. Delivery Date now lives inside the notes block as a small kicker + value row above the notes body, and the meta grid only renders Payment Terms / Vendor Account (2 cells or none). Applies to both the PDF template (purchase-order.hbs with a new .date-row style + hasSummaryNotes flag) and the Excel generator (DELIVERY block condensed with date + notes, meta row-layout recomputed to skip empty strips entirely).
Inventory stock list dropped the duplicate "Location" meta cell. Location is already rendered as the uppercase kicker under the tenant wordmark in the header, so repeating it as a dedicated meta cell was duplicative (and rendered as a lonely full-width bar when no filter was applied). Meta grid now renders only when filterLabel is set, otherwise no grid at all. Applies to inventory-stock-list.hbs and InventoryExportService Excel output.
Inventory ship audit — 8 pre-merge fixes from the 2026-04-20 code sweep (#1249, docs/reviews/inventory-module-ship-audit-2026-04-20.md):
Dashboard → Create PO route. Reorder "Create PO" CTA targeted /inventory/purchase-orders/add, which is not a real route in the inventory module. Now navigates to /inventory/purchase-orders/new; PurchaseOrderCreateComponent.checkForPreFillState() unwraps history.state.prefill (the list-modal shape) and the legacy flat shape, then clears all prefill keys from history so back/forward nav cannot re-seed the form. Adds a dashboard regression spec.
Physical count explicit-zero persistence. Frontend dropped the backend isExplicitlyCounted flag on load, so a user's explicit 0 count reopened as "uncounted" (blank input, hidden variance). IngredientPhysicalCountItem now declares the field; wasExplicitlyCounted() honors the persisted flag (=== true) and falls back to countedAmount > 0 for legacy rows saved before the flag existed. countedAmount === 0 → null coercion now applies only to untouched rows (both the normal and degraded load paths).
Physical count dirty guard.isDirty previously compared only shelf amount + notes; getChangedItems() also syncs isExplicitlyCounted — page-level dirty state drifted from the save payload and navigation guards could report "no unsaved changes" after an explicit-count action. Both now share a single isRowChanged() predicate that also diffs the explicit-count flag against loadedExplicitSnapshot. Degraded-load path now seeds the snapshots so isDirty doesn't fire on a fresh load.
Physical count KPI math.summaryCountedSoFar used countedAmount > 0, excluding explicit zeros; summaryAvgVariancePercent coerced undefined variance to 0 and averaged across untouched rows, diluting the KPI toward zero. Counted KPI now uses isExplicitlyCounted === true; variance average filters by isExplicitlyCounted && theoretical > 0 && variancePercent != null. Specs updated to lock in the new semantics + new regression tests for explicit-zero and cleared-count cases.
Physical count list empty state. Zero-results on an active filter rendered the first-time onboarding overlay (blurred table + "Start Your First Physical Count"). First-time overlay is now gated on !hasActiveFilters; a filtered-empty message + Reset CTA matches the receiving-list pattern. Table gets inert when the blur overlay is active for correct focus/AT semantics.
Stock adjustment numeric validation.type="text" input accepted arbitrary non-numeric characters and the save button disabled only on adjustmentNewLevel == null — the delta display could render NaN and the click would round-trip to a 422. New canSaveAdjustment getter requires a finite non-negative number; onAdjustmentLevelChange normalizes via Number.isFinite and clamps negatives; onAdjustmentKeydown filters to [0-9.] while allowing Ctrl/Cmd/Alt combos so paste/copy/select-all still work; onAdjustmentPaste splits on . (robust against multi-dot paste) and routes through onAdjustmentLevelChange instead of mutating input.value.
Receiving list row-click consistency. Every row declared [rowClickable]="true" but the handler read item.draftReceiptId on the paginated-table's {item, event} emit payload — every row click was a silent no-op. onRowClick now accepts both payload shapes, routes draft rows to the receiving modal, and navigates non-draft rows (upcoming, completed, overdue) to the PO detail with returnUrl=/inventory/receiving. formatExpectedDate gained an isFinite guard so a malformed ISO renders "-" instead of "Invalid Date" (CLAUDE.md API/Performance rule).
PO edit save button.[disabled] bound only to !selectedVendorId || isSaving, so on new POs the button looked ready while the form was invalid because the expected-delivery-date was empty. New canSaveOrder getter composes vendor + isSaving + editFormRef.form.invalid; stays disabled until @ViewChild('editForm') is populated so the first-render window can't bypass the form guard. Existing ngAfterViewInitcdr.detectChanges() already mitigates ExpressionChangedAfterItHasBeenCheckedError for the new ViewChild-dependent binding.
Hardened through code-simplifier, code-reviewer, Gemini 3.1 Pro (Vertex AI) CTO review, and a post-open GitHub review pass (claude-bot + Codex + Gemini Code Assist). Five rounds in total — two critical findings (degraded-load snapshot gap, canSaveOrder fallback default), two UX findings (modifier-key pass-through, DOM-mutation-in-paste), and one legacy-data finding (isExplicitlyCounted strict-null-check downgrading pre-migration positive counts) all addressed.
PO reorder + auto-generate now include Bulk KPL PAR uplift (#1250, docs/reviews/kpl-bulk-vs-orders-inventory-verification-2026-04-20.md). PurchaseOrderService.calculateUpcomingDemand() previously only queried ProductionDaysRecord with type: 'Execution' — the toMake amount on Bulk records (which represents bulk recipe production when a user sets parStock above actual order demand) was never read. Tenants who set an explicit Bulk PAR above actual orders saw their ingredient reorder recommendations under-order the difference, because the PAR-driven extra production wasn't visible to PO demand.
Unified KPL query now fetches type: {inq: ['Execution', 'Bulk']} in a single round-trip. Execution records feed kplRecipeAmounts as before; Bulk records feed a new bulkUpliftAmounts map containing only the PAR uplift — max(0, toMake - amountUsedInWindow) — so the order-driven portion (already captured by Execution / order expansion via bulk-as-sub-recipe traversal) is not counted twice.
Partial-window overlap handling. A Bulk KPL's periodStartDate..periodEndDate (often 7+ days) routinely extends beyond the reorder window (3–14 days). Subtracting the full amountUsed over-subtracts → under-orders the out-of-window portion. The fix sums the per-day amountUsedByExecutionDay breakdown over datesInWindow to get the exact in-window order contribution. Legacy records without the breakdown fall back to total amountUsed (documented tradeoff: mild under-order risk vs. missing data).
Reason-code gate.kplFound now counts Execution + Bulk records so a Bulk-only window (e.g., PAR already satisfied by on-hand, zero uplift) surfaces as NoTrackedIngredientDemand (yellow "data exists, no demand") rather than being mis-routed through the NoOrdersInWindow "no data" branch. The truly-empty case (zero orders, zero Execution, zero Bulk) still correctly hits NoOrdersInWindow. Logger reports kplFound (Execution), bulkFound, and bulkUpliftRecipes separately.
Yield batch-fetched once for the union of Execution + Bulk recipe IDs — no extra DB round-trip over the prior code path.
Date invariant documented. Bulk records carry a single date marking the day the kitchen actually produces (ingredients consumed in one shift, then served from stock across periodStartDate..periodEndDate). Filtering by date: {inq: datesInWindow} therefore correctly captures every production event whose ingredient demand falls in the window — past-dated bulks are already reflected in current on-hand via getOnHandBulk.
Nine unit tests lock in the contract: PAR uplift when PAR > orders; zero uplift when orders cover; combined Execution + Bulk expansion inputs; unified query shape; legacy records without amountUsed; orders + Bulk same-day double-count regression; partial-window overlap math; Bulk-only window → NoTrackedIngredientDemand; truly-empty window → NoOrdersInWindow (control).
Hardened through code-simplifier, code-reviewer, Gemini 3.1 Pro (Vertex AI) CTO, and a post-open GitHub review pass (claude-bot + Codex + Gemini Code Assist). Three substantive findings fixed (CRITICAL partial-window under-count, P2 kplFound Bulk-only misrouting, 🟠 date-invariant documentation); MEDIUM legacy-fallback tradeoff and LOW reason-code coupling acknowledged.
Added
Backend exports redesign — Phase A (shared helpers + Excel rewrites + field mapping) — rewrites the Excel generators for Purchase Orders, Physical Counts (Sheet + Report variants), and Ingredient Inventory to match the locked "After" mockup at /tmp/demi-mockups/exports-design-system.html. Excel outputs now get document-specific sheet names (no more Sheet1), frozen column header rows, dynamic AutoFilter ranges, real pageSetup with printTitlesRow, and status pills rendered as cell fills (not colored text). Two new shared helper modules: backend/src/services/helpers/export-format.helper.ts (formatDate / formatAddress / formatPhone / formatCurrency / getTenantHeader — address formatting canonicalizes country codes and rejects empty segments; phone strips the legacy CA- / US- prefix concatenation bug) and backend/src/services/helpers/excel-style.helper.ts (getStatusFill with ARGB palette, applyHeaderBlock, applyPrintSetup, setColumnHeader, freezeHeaderRow). fetchLogoAsDataUri extracted to logo-fetcher.helper.ts with an added fetchLogoAsBuffer variant for ExcelJS workbook.addImage embedding (HTTPS SSRF guard preserved). Field-mapping gaps closed: PurchaseOrder.currency field added (derived from tenant.configuration.address.country at create — US → USD, else CAD), SubTenantConfiguration.receivingContactName added (surfaces as "Attn:" on PO ship-to block when set, omitted otherwise), PO export service now loads vendor address via include: [{relation: 'address'}] and populates uomLabel on each line item via a batch UnitRepository.find({where: {id: {inq: [...]}}}) lookup, Ship To name now correctly assembled as ${tenant.name} — ${subTenant.name} (was previously dropping the tenant and showing only subtenant). Empty fields are now hidden entirely in the meta grid / KPI summary — no em-dash placeholders. Internal statusTimeline dropped from vendor-facing POExportData payload (was leaking audit trail onto the vendor doc). Gemini 3.1 Pro pre-merge review surfaced two real findings, both fixed: safeText regex now catches leading whitespace (/^\s*[=+\-@]/) so " =1+1" can't bypass the formula-injection guard; new logo-fetcher.helper.test.ts locks in 13 assertions on the SSRF guard + content-type → extension mapping. PO Excel drops the Shipping totals row (field isn't captured by the UI); Inventory drops the "Currency" meta cell (internal doc, not needed); Physical Count status pill moved to dedicated row 4 F4:G4 (was crowding the meta kickers in column 2). PDF generators still point at the external PDF microservice — PDF HTML template rewrites ship in a follow-up Phase C (separate repo); only Excel + payload shape ship in this PR. New backend/src/types/cacheable-lookup.d.ts is a minimal type stub for the untyped cacheable-lookup@6.1.0 dep introduced by #1237 (project tests run on compiled JS, so the missing declaration was silently masked until tsc --noEmit surfaced it during this work). 159 passing unit tests (146 baseline + 13 new logo-fetcher assertions).
Inventory pre-production hardening phase 3 — rounding at persistence boundaries + Typesense sync on depletion (#1243). New backend/src/utils/decimal-rounding.util.ts (roundForPersist, sumForPersist) applied at every ledger write (depletion, goods receipt, on-hand sum) so accumulated IEEE-754 drift can't reach DECIMAL(16,8) columns. MySQL was truncating unrounded floats producing UI-vs-ledger divergence on bulk receipts. Post-commit ingredientRepo.syncToTypeSense() now fires on depletion success (outside the tx try/catch — a sync failure can't rollback a committed ledger). 10 unit tests lock the drift-free contract.
Inventory pre-production hardening phase 4 — PO compare-and-swap locks + bulk-receive version fix + permission matrix (#1244). cancel(), bulkSubmit(), bulkCancel() on purchase orders now use updateAll({status: X}, {id, status: {inq: [...]}}) with bypassCache: true so concurrent status transitions fail with 409 Conflict (status race, not 400 BadRequest). bulkMarkAsReceived now captures the bumped version from updateReceiptItems before calling confirmReceipt — every multi-item bulk-receive previously 409'd on OCC because the stale receipt.version was passed through. Canonical permission matrix at docs/inventory-permission-matrix.md documents every inventory endpoint → @authorize mapping (audit result: clean across 5 controllers).
Inventory pre-production hardening phase 5 — frontend error handlers + wizard dirty guard + UI polish (#1245). stock-history.component.ts load failures now log, toast, and navigate back instead of silently stranding the operator on an empty view. receiving-list.component.ts and goods-receiving-route-wrapper.component.ts replace non-null-asserted onHidden! with safe onHidden?.pipe(...) so a rapid dismiss-before-subscribe can't blow up. Inventory-setup wizard close() prompts via AlertsService when dirty (selection changes or non-zero stock entered on new items); Number.isFinite guard in executeConfirmAndSave() bounces invalid stock back to step 2 with an actionable inline error instead of letting the backend reject with 400. Dashboard recommendations *ngFor gained trackBy: trackByVendorId to prevent DOM thrash when toggling a single vendor card. UI polish: dropped the colored left-border accent on .quick-action-banner, replaced the custom .table-empty div on PO-create with <um-empty-state>, swapped <i class="fa fa-redo"> for <fa-icon>.
Inventory pre-production hardening phase 6 — vendor XSS strip + field maxLength + E2E regression guards (#1246). New backend/src/utils/sanitize-vendor-strings.util.ts strips HTML tags from vendor free-text fields (name, mainContactName, notes, deliveryInstructions, accountNumber, paymentTerms, url, image). Two-pass (strip tags → decode </>/</< entities → strip again) closes the unterminated-tag and numeric-encoded-tag bypasses that a naive <[^>]*> regex misses. Applied at the top of VendorController create / updateAll / updateById / replaceById. maxLength constraints added to previously unbounded fields (name 255, mainContactName 255, url 500, image 500, notes 5000). E2E guards in tests/e2e/inventory/inventory-management.spec.ts cover the wizard dirty-close prompt (PR-16) and cross-tenant IDOR handling (phase 1).
Inventory reorder recommendations now open the modal Create PO — clicking "Start PO" from the dashboard's reorder-recommendations panel opens the modal create-PO experience with the vendor and suggested line items pre-populated (same path as + Create PO), instead of dumping the user into the legacy full-screen form.
Client-side inventory print — the dashboard's Print action renders a local print view (InventoryPrintService + PrintableInventoryComponent) instead of round-tripping a PDF from the external service. Opens the browser print dialog in under a second vs. the prior ~15-20s wait, matching the existing invoice-print pattern. Backend exposes ?format=json on /ingredient-inventory-config/export for the payload.
Changed
ProductionDaysRecord.depletionStatus now reports 'partial' on Typesense-sync-only failures — the enum value existed but was never set. IngredientDepletionService.calculateAndRecordDepletion now returns a typesenseSyncFailed flag; production-data.service.ts sets depletionStatus = 'partial' when the ledger commits but one or more post-commit syncToTypeSense() calls reject. Ledger count is correct but search results may lag until sync is retried. Ops distinguishes this from 'failed' (ledger itself did not commit) via the dashboard badge.
Inventory PDF/Excel exports align with the dashboard's 5-band status logic — "Out of stock" (0 on hand), "Critical" (<50% PAR), "Below PAR", "In stock", "No PAR". Below-PAR summary count now includes out-of-stock items, matching the dashboard KPI. PAR column displays primary units only (secondary line removed). Backend export now treats parLevel === 0 as "In stock" (explicitly "no minimum needed"), matching the repository + frontend logic.
Expected Delivery Date is required when creating a PO manually — the Create PO modal and reorder Start PO flow now surface the * required indicator and the form blocks save until a date is chosen. Editing existing POs (including auto-generated drafts created without a date) is unaffected.
Received metadata split on received POs — the readonly PO view now shows "Received by {name}" and "Received on {full timestamp}" on separate rows to match the inventory print layout.
Optimistic bulk-cancel on the Purchase Orders list — bulk-cancelling draft/submitted POs flips each row's status badge to "Cancelled" immediately on API success, instead of waiting for a full reload.
Purchase Orders refresh button shows progress — the header refresh icon now spins and disables while a manual refresh is in flight, with a safety timeout that clears the spinner if the underlying list never emits.
Vendor → PO integration surfaced end-to-end (#1248) — selecting a vendor on the Create PO modal now auto-populates the Notes textarea from the vendor's "Default PO note" (vendor.deliveryInstructions). Preserves custom edits: notes only swap when the previous value still matches the prior vendor's default, tracked via _lastAppliedVendorNoteDefault. Readonly PO view now surfaces orderingEmail (as an "Orders" row), the vendor's structured street address, and renames "Submitted"/"Submitted by" → "Placed on"/"Placed by" (matching the existing Received on/Received by pattern, formatted as medium for consistency). The inline vendor caption below the Create-PO vendor select prefers orderingEmail over the rep email when both are set, since ordering email is the address POs will actually be sent to. Ingredient-name cell in line items dropped the ↑/↓ N% price-delta indicator — kept on the inventory dashboard, removed from PO context where it was noise.
Credit terms: removed Net 8 from the canonical CREDIT_TERMS constant — not a standard payment term.
Added
Vendor screen overhaul — rebuilt the Add/Edit Vendor modal on Demi's current settings-section layout (matches the edit-customer pattern). The modal now surfaces fields that already existed on the backend but were hidden in the UI, plus a new structured address:
Structured address via the global <um-address-input> (Google Places autocomplete) — persisted through a new addressId FK on Vendor and a shared Address row, mirroring the customer pattern. Vendor controller overrides create / updateById / replaceById to create-or-update the Address row and set addressId atomically. Migration migrate-vendor-address-and-index.ts adds a composite (tenantId, subTenantId, accountNumber) index and an addressId index online (ALGORITHM=INPLACE, LOCK=NONE).
Account #, Rep name (= mainContactName), Rep email (= email), Ordering email (tooltipped), Credit terms (ng-select backed by the new shared CREDIT_TERMS constant, with [addTag] so legacy DB values like Net 45 render and a trim+dedupe callback prevents duplicate tags), and Default PO note (= deliveryInstructions, tooltipped, maxlength="500") — all with aria-invalid bindings and markAsTouched on save so required-field errors light up on the first click.
Vendor list now includes an Account # column (sortable, backed by the new composite index).
Logo uploader switched from the legacy <um-upload-image> to the shared <um-image-uploader-row> component (matches customers / ingredients).
PO export DTO extended with orderingEmail, accountNumber, and vendor address so downstream PDF/XLSX templates can render them.
Ingredient importer populates vendor fields opportunistically — VendorInfo DTO extended with optional vendor_email, vendor_phone, vendor_account_number, vendor_payment_terms, vendor_ordering_email, vendor_postal_code, vendor_country_division, vendor_address2. When the upstream AI extractor supplies any of them, the importer stamps them onto newly-created vendors and best-effort fills empty columns on existing vendors (never overwrites user-populated fields). Email values are validated via the shared validateEmail helper; invalid values are dropped with a warning log. Extracted addresses now create a structured Address row instead of being stuffed into the free-text notes field.
VendorRepository write-path normalization — overrides create / createAll / updateById / updateAll / replaceById to collapse empty-string email / orderingEmail / accountNumber / phone / paymentTerms to null, so the PO send fallback (orderingEmail || email) can't land on "" and PATCH can explicitly clear a column.
Shared CREDIT_TERMS constant — mirrored in frontend/src/app/shared/constants/credit-terms.constant.ts and backend/src/constants/credit-terms.constant.ts, consumed by both edit-customer (refactored off its inline literal), the new edit-vendor, and the ingredient importer's normalizePaymentTerms helper so canonical values don't drift across the stack.
Vendor address lifecycle hardening — vendor controller uses delete-first-then-update semantics when a client clears the address (prevents orphans if the Vendor update fails); orphan-cleanup failures on create / clear / replace are escalated to Sentry with tags: { context: 'vendor-address', kind, vendorId, addressId } per the project Sentry convention. Address payload on create now requires address1 + city minimum (aligned with importer strictness) — partial country-only or postal-only payloads are rejected with HTTP 400 instead of polluting the Address table.
Ingredients bulk editor: explicit save/discard pattern — replaced auto-save-on-debounce with a Save/Discard banner (matching the recipes bulk editor UX). A dirty-state service (IngredientBulkEditorChangesService) tracks which rows have changed and emits a reactive count. Changes are batched and saved sequentially; partial failures leave failed rows dirty so users can fix and retry without losing context. Navigation away with unsaved changes triggers a confirmation dialog via the existing UnsavedChangesGuard.
Ingredients bulk editor: Par stock and Track inventory columns — IngredientInventoryConfig records (separate API model) are batch-fetched after each page load and merged into the row data. Par stock accepts a number input; Track inventory uses a toggle switch. Both fields participate in dirty tracking and are saved (or upserted if no config exists yet) alongside the ingredient on batch save. A skeleton/spinner is shown in those cells while configs are loading.
Ingredients bulk editor: 3-state status selector — replaced the Active/Inactive toggle with um-status-selector [includeDraft]="true" so Draft status is now selectable directly in the bulk editor.
Ingredients bulk editor: Label name moved to Procurement group — Label name column relocated from the Core attribute group to the rightmost position in the Procurement group, matching the edit-recipe form layout.
Search and filters in bulk mode (Ingredients) — when Typesense search is active, the Typesense table is kept alive via [hidden] (instead of destroyed via *ngIf) so its data feed continues in bulk mode. A dedicated filter strip (search input, Status, Ingredient Categories dropdowns, Reset button) is shown above the bulk editor. Filter controls lock (opacity + pointer-events: none) when unsaved changes are present to prevent silent data loss from a page re-fetch.
Search and filters in bulk mode (Sub-Recipes / Products) — the existing filter bar (Status, Recipe Categories, Workflows) is already rendered outside the list/bulk mode guard, so it remains visible and functional in bulk mode. Filter controls lock identically when unsaved changes are present.
Products (Meals) bulk editor: Default retail price and Default wholesale price columns — two number inputs (step 0.01, min 0) appear in a Pricing column group only when recipeTypeName === 'Meal'. Both fields are tracked for changes, restored on Discard, and included in the batch save payload.
Fixed
PO Export PDF / Export Excel from the single-PO 3-dot menu (#1248) — staging was 500'ing because every Export (even single-PO) routed through the async Cloud Tasks + GCS batch pipeline, and two infra gaps hid the real cause: the Cloud Tasks service agent lacked roles/iam.serviceAccountTokenCreator on the backend SA (so dispatch failed before enqueue), and /internal/process-batch was rejected by the app-level authorize middleware with a 403 before its own OIDC verification ran (loopback4-authorization is secure-by-default when no @authorize decorator is present). Fixes: (1) frontend routes single-PO Export PDF/Excel through the sync GET /purchase-orders/{id}/export endpoint (purchase-order-edit.component.ts:downloadPdf/downloadExcel → poApi.exportPo() blob download) — works cross-cloud via utils/gcp-auth.ts, no Cloud Tasks needed; batch pipeline preserved for multi-PO list selections. (2) backend/src/application.ts extends AuthorizationBindings.allowAlwaysPaths to cover /internal/process-batch, /internal/reap-stale-jobs, /internal/record-ai-feedback, /internal/process-ai-feedback-export, /internal/purge-ai-feedback (inline comment warns that the library matches via req.path.indexOf(path) === 0 — future /internal/* routes MUST call verifyOidcToken as their first line). (3) BatchExportService catch block now writes Cloud Tasks failures to process.stderr alongside logger.error (band-aid — the Winston/Pino transport silently drops error-level lines on Cloud Run; full logger fix tracked for pre-cutover). (4) PurchaseOrderExportService.generatePdf rejects unresolved PDF_SERVICE_URL placeholders (strings starting with [) with a 503 fallback so AWS prod with unconfigured env vars gets a clean error instead of a DNS failure. Staging IAM grant applied; prod Cloud Tasks API enabled; remaining prod infra wiring (queue create, GCS bucket + TTL, env vars) documented in docs/infrastructure/gcp-cutover-runbook.md §P3f + P30 checklist. Verified by Gemini 3.1 Pro CTO (2 rounds).
Vendor edit save 422 on existing vendors (#1248) — PATCH /vendors/{id} was rejecting the nested address payload as an additional property because the request-body schema getModelSchemaRef(Vendor, {partial: true}) sets additionalProperties: false, even though resolveAddressId() inside the handler specifically reads and strips vendor.address before the repo write. Switched POST + PATCH to the permissive {type: 'object'} schema (matches the customer controller pattern) and added a focused validateVendorEmails() helper that re-enforces format: 'email' on email + orderingEmail — otherwise the loose schema would drop the model-level format constraint.
Empty-state table rows light up teal on hover — the global .table-hover tbody tr:hover rule in _light.scss:50 was colouring the <tr> that renders Empty list. inside every um-paginated-table, which implied clickable content where none exists. Added class="empty-row" to the template row and a scoped :host ::ng-deep override in paginated-table.component.scss that cancels the hover + focus-within backgrounds. Applies to every empty table in the app.
AWS prod boot-time schema sync restored — PR #1236 gated app.migrateSchema off by default for NODE_ENV=production (correct for GCP Cloud Run, where the pre-deploy migrate Job owns schema sync). AWS prod has no equivalent pre-deploy Job — boot-time migrateSchema has always been the only schema-sync mechanism. With AWS prod ~4 weeks behind on deploys (ai_insight_feedback table, goods receipt version/variance columns, vendor addressId FK, physical count notes, etc. all accumulated on main), a gated-off boot call would leave prod running without those new columns/tables and break any feature that expects them. Fix: set ENABLE_BOOT_MIGRATION: 'true' in backend/server-config/ecosystem.config.js env_production block. The existing env-sync machinery in backend/aws/after-install.sh auto-injects new keys into /var/pm2/ecosystem.config.js on every deploy, and backend/aws/stop.sh → start.sh runs pm2 kill && pm2 start $CONFIG_FILE --env production which fully reloads env vars. Hardened through Gemini 3.1 Pro CTO review (8 findings: 3 CRITICAL disarmed by codebase evidence, 3 HIGH disarmed or mitigated, 2 MEDIUM accepted). All new columns since the last prod deploy either have default: in the model or are nullable, so rolling CodeDeploy (3 EC2s, sequential) is forward-compatible — old code on EC2-2/-3 continues inserting successfully while EC2-1 runs the schema migration.
Migrate schema drop-recreate loop + prod schema parity — LoopBack's loopback-connector-mysqlexistingSchema:'alter' mode actively DROPs any DB index not declared in model.settings.indexes[] or named after a property with index: true (see migration.js:310-357). The boot-time app.migrateSchema call at backend/src/index.ts:54 was wiping every composite the post-schema scripts (migrate-vendor-address-and-index.ts, migrate-ai-insight-feedback.ts) created on every Cloud Run web service restart — including the UNIQUE idx_aif_idempotency Cloud Tasks at-least-once delivery guard. Fixes applied after 3 rounds of Gemini 3.1 Pro CTO review:
Gated boot-time migrateSchema behind ENABLE_BOOT_MIGRATION env var (backend/src/index.ts). Defaults ON for local dev (NODE_ENV ≠ production/staging) so fresh checkouts still bootstrap. Production/staging Cloud Run web instances no longer run DDL on boot, which also eliminates a metadata-deadlock hazard when multiple instances scale up concurrently. The pre-deploy migrate Cloud Run Job remains the sole owner of default-DS schema sync.
Re-declared the 5 ai_insight_feedback composites in settings.indexes using strict keys: {<DB column name>: 1} syntax and explicit options: {unique: true} on idx_aif_idempotency. PR #1233's earlier attempt used JS property names (idempotencyKey) and hit errno 1072 — the correct approach is literal DB column names (idempotency_key, created_on, etc.). All 5 composites fit within the 3072-byte InnoDB key limit so LoopBack's prefix-free buildIndexes() output is safe.
Vendor composites stay post-schema-only because tenantId(255) + subTenantId(255) + accountNumber(512) = 4088 bytes exceeds the InnoDB limit without prefix. Model comment documents the trade-off.
Vendor.addressId TEXT → BIGINT normalization in migrate-vendor-address-and-index.ts with 4 hardening safeguards: REGEXP anomaly guard (^[0-9]+$), UPDATE SET NULL WHERE TRIM() = '' before the cast (strict-mode-safe), information_schema.STATISTICS existence check before conditional DROP INDEX idx_vendor_address (MySQL has no portable IF EXISTS), and row-count warning log over 10K. Addresses schema drift on GCP staging and AWS prod where the column was created as TEXT by historical migrations.
New unit spec migrate-vendor-address-and-index.test.ts (6 tests, all passing) covers: table-not-found early return, BIGINT skip, non-numeric anomaly abort, clean TEXT → nullify + ALTER, pre-existing index drop ordering, and dry-run purity.
Typesense client: keep-alive + cached DNS to survive bursty reindexes — the Typesense Node client was initialized with additionalHeaders: { Connection: 'close' }, forcing a fresh DNS lookup + TCP handshake + TLS negotiation on every request. On GCP Cloud Run, a full reindex (~15k documents) exhausted the per-VPC Cloud DNS query budget, surfacing as ENOTFOUND getaddrinfo failures against a cluster that was healthy in the Typesense Cloud UI. Replaced with explicit http.Agent / https.Agent configured with keepAlive: true + a module-level cacheable-lookup resolver (60s TTL / 10s errorTtl) so bursts reuse sockets and share resolved addresses instead of hammering the VPC resolver. New unit test typesense-connection.test.ts locks the agent config + the absence of Connection: close against accidental regression. Adds cacheable-lookup@^6.1.0 to backend/package.json.
Founder Cockpit: session JWT now authenticates the metrics proxy — the cockpit was rendering "Task queue temporarily unavailable" because the Angular data service called the LoopBack proxy with raw HttpClient (no Authorization header), and the backend's @authenticate(STRATEGY.BEARER) gate returned 401. Switched FounderCockpitDataService to ApiService.apiRequest so the user's session JWT is attached automatically (same pattern every other admin service uses). Also dropped the leading /api/ from the LoopBack route; apiURL already prefixes /api, so the controller now registers /cockpit/metrics and the frontend calls cockpit/metrics. New backend/src/controllers/cockpit.controller.ts added.
Founder Cockpit: resilient error handling across both data methods — getPendingTasks() now has its own catchError returning an empty task list (matching its independent-error-boundary JSDoc contract), and the shareReplay(1) error reset (metrics$ = null) moved from getDashboardData into getMetrics itself so both methods recover after a proxy failure. Backend upstream-body validation hardened with Array.isArray() guard. Spec rewritten from HttpClientTestingModule to jasmine.createSpyObj<ApiService> with all prior assertions preserved plus a new retry-after-error test.
Vendor screen overhaul: staging post-deploy fixes (hotfix for the Added entry above) — four UI issues surfaced on GCP staging after the vendor overhaul shipped: (1) modal close X clipped by a w-100 wrapper in the header, (2) address autocomplete rendered expanded (mockup called for collapsed-by-default), (3) Vendor name label missing the required-field asterisk, (4) new-vendor "Add" form opened in readonly mode because GenericEditComponent.readonly defaults to true and form controls stayed hidden behind *ngIf="!readonly". Fixed by dropping the w-100 wrapper, adding [collapsed]="true" to <um-address-input>, adding the required class to the label, and a tap in itemLoaded that flips readonly = false when !updatedItem.id. Two regression tests added in edit-vendor.component.spec.ts.
GCP staging deploy workflow: migrations now run before service replace — the vendor overhaul surfaced a class of 500s where new backend code went live against an un-migrated schema (Unknown column 'addressId'). deploy-gcp-staging.yml now derives a upmeals-backend-migrate Cloud Run Job from the patched service YAML (inheriting image, env vars, secret refs, cloudsql-proxy sidecar, and VPC network — annotations pulled from the service spec so config drift can't strand the Job) and runs it with --waitbeforegcloud run services replace. maxRetries: 1 absorbs transient Cloud SQL blips without hiding real failures (LoopBack migrations are idempotent).
Inventory module: 20-bug QA batch (data correctness, realtime sync, multi-tenancy, modal parity) — resolved a full QA pass of the goods-receiving / PO modals shipped in #1217, #1220, #1223:
Pack description format aligned across FE and BE — backend generatePackDescription was producing "12 x Gram" / "Case" strings while the frontend's IngredientProcurement.getNiceNameDetails() renders "120 g box, case of 12". Ported the FE algorithm to the BE (uses shelfAmount + shelfUnit.symbol + purchaseUnit.name, plus master-case form when quantityPerPurchaseUnit > 1). Added a one-shot backfill-pack-description.ts script (tenant-aware, --dry-run support, batched 500 rows) to rewrite existing rows. Script is designed to run as a GCP Cloud Run Job.
Purchase-order realtime sync to Receiving list — submit() now publishes an SSE event via Redis-backed EventBusService, and all PO/goods-receipt event model names are normalized to lowercase ('purchaseorder', 'goodsreceipt') to match the frontend filter. Goods-receipt service now publishes on create/update/confirm so partial-received state propagates across tabs without refresh.
Submitted-by attribution — added submittedBy JSON column to PurchaseOrder; submit() endpoint resolves the current user via resolveUserIdentity() and stores them alongside submittedAt. Rendered on the PO view as a "Submitted by" row.
createDraft payload completeness + date validation — frontend was dropping expectedDeliveryDate and notes on first save. Both fields now round-trip through the FE service, BE controller (with ^\d{4}-\d{2}-\d{2}$ pattern validation), and BE service.
Ship To tenant label — PO detail response now includes tenant.name and subTenant.name via @belongsTo relations; the Ship To block renders "UpMeals — Vancouver" from the PO's persisted relations (not session state), preserving historical integrity if users later switch subtenants.
Receiving list: per-row action state + trackBy — clicking Receive on one PO no longer greys out every other row's button; trackBy on purchaseOrderId prevents full list re-render on state changes.
Typesense refresh icons — added standard sync-alt refresh buttons to PO list and Receiving list headers, with [disabled] + fa-spin during fetch to prevent spam-click.
Goods Receiving modal polish — modal header now matches the app-wide modal-header pattern; Quick Receive banner toned down from neon teal to neutral gray with a teal accent stripe; variance panel softened from alarm-yellow to neutral "Reason (optional)" prompt; entering a short-received qty keeps the row checked (variance surfaces independently); qty input validates min=0 on FE + BE; Receive All no longer auto-saves (explicit Save Draft with subtle pulse when dirty).
PO create/edit modal — unit cost is now read-only in the line-items table (cost is procurement-level); live Order Subtotal in the sticky footer; Export (PDF/Excel/Print) moved out of the 3-dot menu into a dedicated dropdown beside Mark as Sent; Mark as Sent tooltip relocated to a keyboard-focusable info-circle icon with a shorter copy.
Duplicate units in recipe editor dropdown (GCP staging only) — unit selector showed 4× each symbol (g, oz, etc.) on GCP staging. Root cause: the Calgary ETL (backend/src/scripts/calgary-migration/) blindly INSERT'd globally-shared reference tables (Unit, RecipeType, Language) in Wave 1 as if they were tenant data, duplicating UpMeals' rows (148 duplicate Unit rows + 6 duplicate RecipeType rows on staging; AWS prod untouched). Fix has three parts: (1) new globalReferenceTable: { naturalKey } flag on TableConfig in wave-config.ts; migrate-table.ts now UPSERT-merges these via natural-key lookup and seeds idMap without insert — prevents recurrence on prod cutover. (2) ER_DUP_ENTRY handler in migrate-table.ts now seeds idMap for non-auto-increment PKs (fixes silent idMap gap for Language). (3) New one-shot script backend/src/scripts/cleanup-duplicate-global-reference-rows.ts repoints FK references from duplicate IDs to lowest-id canonicals (chunked UPDATEs at ≤5k rows/statement) and adds UNIQUE INDEX (naturalKey, deleted_on) as defense-in-depth. validate-migration.ts extended with a post-ETL duplicate-key check. Staging cleanup is user-initiated; no prod impact.
Bulk editors: search, filters, and header fixes across all three pages — resolved display and functional bugs introduced in the initial bulk editor enhancement:
Ingredients header controls in bulk mode — the full Actions dropdown, Import Ingredients button, and Add Ingredient button were visible in bulk edit mode. Replaced with a standalone "Exit bulk editor" button matching the sub-recipes/products pattern.
Missing search/filters on Sub-Recipes and Products bulk editors — added the search bar, Status filter, and Category filter to both pages. The Typesense table is now kept alive via [hidden] (instead of destroyed via *ngIf) so search queries work in bulk mode.
Ingredients bulk search returning no results — the Typesense table's updateQueryParams() was changing the URL, triggering the parent's route handler to overwrite search results with unfiltered API results. Added suppressQueryParamUpdates input and skipQueryParamUpdate parameter to prevent URL updates in bulk mode.
DRY refactor — moved bulk filter state and methods (search, status, category filters with debounced search subject) from IngredientsManagementComponent to the shared GenericManagementComponent base class. Recipes override rebuildBulkExternalFilter() to use the correct Typesense field name per recipe type (productCategoryName for Meals, recipeCategoryName for Sub-Recipes).
Recipes pagination layout — restructured to match the ingredients pattern (removed unnecessary wrapper divs, fixed mr-sm-4 placement, added null safety on the guard condition).
Recipes status column width — widened from col-width-8 to col-width-10 so the status dot and full text display without truncation.
Goods receiving: modal footer clipped by unconstrained Angular host (C1 hotfix for #1220) — the scoped overflow: hidden on .modal-content + overflow-y: auto on .modal-body landed in #1220 but never engaged because um-goods-receiving renders as a plain display: block host between them, breaking the flex chain. Body grew to its intrinsic 1335 px and the footer (Cancel / Save Draft / Confirm Receipt) was clipped below the 900 px viewport on any PO with >~6 line items. Added :host { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; max-height: 100%; } in goods-receiving.component.scss so the host is a sized flex parent of the body. Verified on staging-gcp (PO-00003, 13 items @ 1440×900).
Goods receiving: summary endpoint didn't mark draft-receipt POs as in_progress (C2 hotfix for #1220) — #1220 updated the row-state logic in receiving-list.component.ts so the list button flips to "Continue" when receivingStatus === 'in_progress' && draftReceiptId, but the receiving-summary SQL only derived receivingStatus from po.status — a draft receipt against a submitted PO stayed 'upcoming', so the button stayed "Receive". PurchaseOrderRepository.getReceivingSummary CASE now returns 'in_progress' when EXISTS (draft GoodsReceipt) regardless of po.status, and the overdue / upcoming filter clauses exclude POs with drafts so one PO can't appear in two buckets. Added regression test in backend/src/__tests__/unit/repositories/purchase-order.repository.test.ts.
Goods receiving: post-ship QA parity pass — closed 16 findings surfaced by design review of the shipped modal against the approved mockup:
Modal body overflow clipped the footer — global .modal-dialog-scrollable .modal-content { overflow: visible } rule (used to unclip ng-select dropdowns) caused .modal-body to grow past its container. Added a scoped .goods-receiving-modal override in _modal.scss that restores overflow: hidden on the content and overflow-y: auto on the body so line items scroll internally and Save Draft / Confirm Receipt remain pinned at the bottom.
Duplicate POST /api/goods-receipts on repeat "Receive" clicks — startReceiving() unconditionally POSTed a new draft. It now branches: if item.draftReceiptId is already populated (a prior click created a draft), it opens that draft via GET instead. After a successful create, draftReceiptId and receivingStatus are updated locally so the row's button flips to "Continue" without a list reload.
Variance reason incorrectly required — the approved mockup specifies variance reasons as optional, but hasMissingVariances() was wired into the Confirm Receipt disable gate. Removed from the disable logic; the variance panel still surfaces so reasons can be captured when useful.
"Quick Receive" banner disappeared after the first typed qty — banner now stays visible while fullyReceivedCount < rows.length and swaps its copy + CTA to "Receive Remaining" once some items are confirmed.
iPad landscape stuck on desktop-sized touch targets — the single @media (max-width: 768px) breakpoint missed iPad landscape (1024–1366 px). Switched to @media (hover: none) and (pointer: coarse) (plus a 768 px fallback) so all touch devices get the 44 px checkboxes, qty buttons, inputs, and footer buttons.
Missing mockup context fields — the context card now shows vendor Contact / Email / Phone and Order Date (pulled from Vendor.mainContactName/email/phone and PurchaseOrder.orderDate). Backend GET /goods-receipts/:id was extended to include these alongside the existing vendor/PO context.
Expected qty column wrapping / misaligned digits — white-space: nowrap + font-variant-numeric: tabular-nums on the expected-qty span.
Qty control redesign — removed the increment (+) button to match the mockup layout (Expected | input | single decrement); receiving is rarely over-qty, adjustments are downward.
Progress counter now counts fully received items — replaced entriesCount (any qty > 0) with fullyReceivedCount (qty equals expected), and surfaced a "N with variance" note in the footer when partial rows exist.
Unit suffix on cost — line-item subtitle now renders $32.00/case when the backend returns a purchase unit name (batched unit lookup in findById).
Dates humanized — receipt date, expected delivery, and order date all run through | date:'mediumDate' (e.g. Apr 15, 2026).
Skeleton loader parity — skeleton rows now include a qty-group column; the loading state shows um-skeleton-button placeholders for the footer actions.
Checkbox keyboard parity — the custom receiving checkbox now activates on both Space and Enter.
Modal width — switched from modal-xl (1440 px, wasted gutter on wide screens) to modal-lg (1152 px) to tighten the single-column list density.
Dead code — removed the unused getProgressPercent() helper on ReceivingListComponent (the value is already denormalized onto each row during mapping).
PO List: Export button dropdown affordance — removed the Bootstrap dropdown-toggle CSS pseudo-caret (too subtle to communicate "this is a dropdown") and replaced with an explicit fa-icon chevron-down at the end of the button label.
PAR Level read-only display: removed ~ approximation prefix — roundForDisplay() was prepending ~ to non-integer values (e.g. ~1.1 Cases). The tilde added visual noise without meaningful precision signal; values are now displayed as plain decimals (e.g. 1.1 Cases).
Inventory UI design review — cross-cutting polish — resolved 13 design findings across the inventory module:
Back navigation buttons — all three sub-pages (Goods Receiving, Physical Count Detail, Physical Count List) now use the standard btn-link + text-primary + fa-icon chevron-left pattern, matching the rest of the app. The error-state back button in Goods Receiving was a secondary button with HTML entity arrow and was corrected to the same pattern.
Vendor dismiss undo: fixed a silent no-op bug where the undo toast "restored" a dismissed vendor recommendation but nothing reappeared. The root cause was that rebuildRecommendationsAfterDismiss() was filtering from the already-mutated recommendations array (vendor already removed). Introduced allRecommendations as an immutable source populated at load time; rebuild now filters from the full source so undo correctly restores the vendor.
Reorder dashboard dead CSS — removed the .btn-edit-pars custom button class (overriding primary border/color on a secondary button) and the .flex-grow local CSS class (replaced with Bootstrap's standard flex-fill).
Stock adjustment modal — current stock value was wrapped in a fake form-control input element. Replaced with a plain span.display-xs to avoid a misleading editable-field appearance.
Physical Count List: inline style removed — style="max-width: 320px" on the first-count description paragraph replaced with .first-count-description SCSS class.
Physical Count Detail: filter dropdown — the custom <select> and associated hand-rolled CSS were replaced with the standard um-filter-dropdown component. Dead filter-pill and filter-area SCSS blocks removed.
Physical Count Detail: close button — the × character close button replaced with the btn btn-link + fa-icon times pattern.
Physical Count Detail: heading hierarchy — section heading changed from <p> to <h2 class="text-sm font-weight-semibold"> with corresponding SCSS update.
PO List: dead print button removed — the *ngIf="isExportingBatch" print-action block that was always hidden (only shown while exporting) removed along with its stale boolean.
PO Edit: Bootstrap spacing tokens — p-3, p-2, gap-2, mb-3, gap-3 replaced with Untitled UI tokens p-md, p-sm, gap-sm, mb-lg, gap-md throughout the edit modal.
Goods Receiving: search filter — um-search-input added above the receiving table. Filtering uses a cached filteredRows property (not a getter) updated by applySearchFilter() at all mutation points (load, search change, receive-all) to avoid DOM thrashing. Progress count and isDirty continue to operate on the full rows array.
Goods Receiving: "Receive all" and back-nav link styles — text-primary added to "Receive all" link button; back-nav buttons corrected to text-primary text-nowrap.
Start count spinner — mr-1 Bootstrap class replaced with mr-xs Untitled UI token; "Starting..." loading text added to match the Physical Count List button.
Dynamic Typesense connection for local GCP development — updated the backend /search/scoped-key endpoint to return the Typesense host URL alongside the scoped API key. The frontend TypeSenseService now dynamically updates its base URL from this response. This fixes "401 Unauthorized" errors when local development is configured to use a remote staging Typesense instance (where the key is signed for staging but the frontend was hardcoded to localhost).
Auto-generate PO list not refreshing without F5 (BUG-A) — handleAutoGenerateResponse() in PurchaseOrderListComponent was missing a store.clearCache() call before reload() in the PoGenerationReason.Success case. All three bulk action paths (submit, receive, cancel) already called clearCache() correctly; the auto-generate path was the only gap. After generating POs, the list now immediately reflects new entries without a manual page refresh.
"Receive Goods" blocked by 409 on orphaned draft receipt (BUG-B backend) — createReceipt() in GoodsReceiptService was throwing 409 Conflict when a draft receipt already existed for a purchase order (e.g., an abandoned receive-goods session). The endpoint is now idempotent: if a draft receipt exists it is returned directly rather than blocking. A structured warning is written to GCP Cloud Logging if the PO was updated after the draft was created (modifiedOn staleness check) so the discrepancy is visible during operations review.
Receipt creation errors showing generic message instead of backend detail (BUG-B frontend) — the receiveGoods() error handler was ignoring the API response body and always showing a hardcoded string. It now uses getErrorMessage() to surface the actual backend error message (e.g., "A draft receipt (#5) already exists") while falling back to a clear generic message for network-level failures.
PATCH 400 silently collapsing edit mode with no feedback (BUG-C) — when updateDraft returned a 400, the edit form appeared to freeze in a read-only state with no toast because: (1) disableEdit() is called before the API request in the base class, and (2) the child's doApiSaveRequest override had no catchError. Added catchError to the PATCH branch that calls errorHandler() (base class: danger toast + enableEdit() restore + isSaving=false) and GenericStoreService.clearStoresForModel() to invalidate the PO list cache. Returns EMPTY for graceful stream completion so re-clicking Save still triggers the API call.
Delete cancelled PO using soft-delete instead of hard-delete — DELETE /purchase-orders/{id} was delegating to SoftCrudRepository.deleteById() which sets deleted=true, meaning cancelled orders were hidden from listings but still existed in the database. The endpoint now uses deleteAllHard({id, status: 'cancelled'}) inside a READ_COMMITTED transaction, atomically enforcing the status guard to prevent TOCTOU races where a concurrent mutation could change status between the check and the delete. Line items are hard-deleted in the same transaction.
"Copy previous PO" triggered while catalog still loading — the copy-previous action in create-mode PO modal would show a "No previous PO found" toast if clicked before the forkJoin fetching vendor catalog and last-PO data completed. Added an isLoadingCatalog guard that shows "Still loading vendor data, try again in a moment" instead of the false-negative toast.
Delete button visible to read-only users — the delete button in the PO edit modal and the delete row action in the PO list were rendered for users without edit permissions. Both now gate on canEdit before checking the cancelled status.
Double-click on "Receive Goods" creates duplicate receipts — clicking "Receive Goods" rapidly in the PO list triggered multiple concurrent receipt creation requests. Added a processingReceiptIds Set that blocks re-entry per PO ID until the request completes (with finalize cleanup).
Blob URL memory leak in print actions — printOrder() and printCount() create blob URLs for PDF preview and schedule a 60-second revokeObjectURL via setTimeout. If the component was destroyed before the 60s elapsed, ngOnDestroy cancelled the timer but the blob URL was never revoked. Both components now track activePrintUrl and revoke it in ngOnDestroy.
Physical count list: API error kills the reload stream permanently — the error handler in the outer subscribe() call terminated the reload$ Subject-based stream on any API error, making filters and date-range changes stop working for the rest of the session. Moved error handling inside switchMap via catchError(() => of([])) so the stream stays alive on transient failures.
Physical count detail: changing unit initializes countedAmount to 0 instead of null — onUnitChange() set item.countedAmount = 0, which displayed "0" in the input but left isExplicitlyCounted = false, causing the variance calculation to show 100% discrepancy for every item the user switched units on. Setting to null correctly empties the input and signals "not yet counted".
Print opens blank page in Chrome's PDF viewer — win.print() was called immediately inside the load event handler, but Chrome's built-in PDF viewer renders its content asynchronously after the document loads. A 500 ms delay is now added before triggering the print dialog, giving the viewer time to initialize. Applies to both the PO edit modal and the physical count list.
Physical count: ingredients with inner pack but no case unit default to recipe shelf unit — the unit priority chain in physical-count-detail was skipping procurementUnitId (inner pack / each), so ingredients that had a procurement unit configured but no purchase unit (case) fell through to the shelf unit (e.g. ml, g). Counting in recipe measurement units is impractical for physical inventory; the chain now tries purchaseUnitId → procurementUnitId → shelfUnitId when no countedUnitId is already saved.
Physical count: theoretical unit defaults to purchase/case unit instead of shelf unit — the unit dropdowns on the physical count detail page (both Theoretical Unit and Counted Unit) defaulted to the shelf unit (e.g. Pound, Gram) on page load, even when a purchase unit (Case, Each) was configured. This made counts impractical because staff count in cases, not grams. The priority chain now selects purchaseUnitId first when it differs from the shelf unit, falling back through procurementUnitId to shelfUnitId. Counted Unit is tracked independently so staff can count in a different unit than the theoretical display.
Physical count: vendor column XSS vulnerability — the Vendor column cell was built with html: \${item.vendorName} ...\` string interpolation passed as raw HTML into the paginated table. Replaced with a #vendorTplng-template` using Angular interpolation binding, which safely escapes all vendor name and code values.
Inventory stock list: missing inventory value column — the stock list PDF export did not include a computed inventory value (on-hand quantity × cost per unit). Added inventoryValue field to the export computed from (costPerPurchaseUnitUSD / shelfAmount) * onHandAmount, with a running total row at the bottom of the table.
Readonly PO modal header missing payment terms and vendor account number — when viewing a submitted or received purchase order, only the vendor name and order date were shown in the modal header subtitle. Payment terms and vendor account number are now displayed as a second subtitle line when present on the purchase order.
PO batch export 500 on GCP staging: wrong Cloud Tasks region default — BatchExportService.dispatchCloudTask() defaulted GCP_LOCATION to us-central1, but Demi's GCP infrastructure is in northamerica-northeast1. Any environment without GCP_LOCATION set would build a queue path pointing at the wrong region, causing a NOT_FOUND / PERMISSION_DENIED from Cloud Tasks and surfacing as a 500 on POST /purchase-orders/export-batch. Fixed the default to northamerica-northeast1 (matching every other Cloud Tasks service in the codebase). Also added a logger.warn at dispatch time if GCS_EXPORTS_BUCKET is not explicitly set so the fallback bucket name is visible in logs before the worker attempts to upload. Added scripts/setup-po-batch-export-infra.mjs to provision the Cloud Tasks queue, GCS bucket, and print IAM / env var steps for staging and production environments.
Goods receiving: quantity inputs showing "0" for unstarted receipts — editQty was initialized to item.receivedQuantity || 0, so every fresh draft showed "0" pre-filled in all quantity cells. Changed to initialize null when receivedQuantity === 0, which renders the inputs as blank with a "0" placeholder. isDirty, snapshots, and payload coercion updated to treat null as zero throughout.
Goods receiving: draft badge showing gray instead of warning amber — badge-gray was used for the "Draft" status badge, inconsistent with the physical-count pattern where badge-warning is the standard for in-progress states.
Goods receiving: missing "Receive All" button — the receiveAll() method existed but had no UI entry point. A "Receive all" link button is now shown above the table when the receipt is in draft mode, filling all quantities to their expected order quantities.
**Goods receiving: table rendered outside *ngIf guard** — um-paginated-table was placed outside the *ngIf="!isLoading && receipt" guard, so an empty table skeleton was always rendered even on load error. The table, action bar, and buttons are now all inside the guard. An error/empty state is shown when the receipt fails to load.
Goods receiving: skipped items in confirmation result show IDs instead of names — after confirming a receipt, skipped items were listed only by numeric ingredientId. They now show the ingredient name (cross-referenced from the loaded rows) plus the reason for skipping.
Goods receiving: optimistic rollback not restoring dirty-guard snapshots — when a saveDraft API call failed, the rollback restored originalQtys but not savedQtySnapshot / savedNotesSnapshot. This left isDirty permanently returning false after a failed save, silently allowing navigation without a warning. Both snapshots are now captured before the optimistic update and fully restored on rollback.
Goods receiving: receiveAll() not re-rendering table inputs — mutating row objects in-place without spreading the array reference left um-paginated-table unaware of the change, so quantity inputs displayed stale values. receiveAll() now spreads this.rows after updating quantities.
Added
Prospect Funnel in Founder Cockpit — new "Prospect Funnel" and "Conversion Rates" cards on the Cockpit dashboard. Paginated HubSpot contact search (up to 5,000 contacts, 50-page cap with ERROR log on overflow) filtered by all demi_outreach_stage values. Stage pills rendered by cadence order (teal = done, amber = due today, grey = pending). "Due today" badge shows contacts where cadence step falls exactly on today's Vancouver date (DST-safe UTC arithmetic). Conversion metrics: Reply Rate %, Demo Booked Rate %, and Reply→Demo % with progress bars and industry-benchmark badges (5–10%, 2–5%, 30–50%).
Contact CRM card — new GET /hubspot/cards/contact-context endpoint + contact branch in DemiSalesOsCard.tsx. Renders lead score, location count, tier tag (green/yellow/default), outreach stage, next follow-up due date (red if overdue), and research notes (truncated to 200 chars). Follows the same auth pattern (isHubSpotBrowserRequest) and error-retry UX as the existing deal card. Place the card via HubSpot → Settings → Objects → Contacts → Record Customization.
Prospect cadence scanner — new POST /internal/scans/prospect-cadence endpoint. Runs 5 parallel per-stage HubSpot searches with server-side date filters (demi_outreach_start_date LTE cutoff). Idempotency via demi_last_cadence_step_created contact property — skips contacts where the current-stage task was already created; logs ERROR with remediation note if the idempotency PATCH fails after task creation. Batch task creation (10 at a time, 200ms pacing). WARN log when per-stage result hits the 100-contact cap. Requires the demi_last_cadence_step_created contact property to be created in HubSpot before first run (see deploy runbook).
Cloud Scheduler cadence scan job — hubspot-prospect-cadence-scan added to setup-cloud-scheduler.mjs. Runs weekdays at 8 AM Vancouver time (before SLA scan at 9 AM). attemptDeadline: 300s with 2 retries and 30–120s backoff. Run GCP_PROJECT_ID=upmeals-staging node scripts/hubspot-sales-os/setup-cloud-scheduler.mjs --apply to provision.- 30-day CC-required trial with Stripe Embedded Checkout — onboarding wizard gains a new Step 5 that mounts Stripe's embedded checkout form inline (no redirect). Users enter a credit card at signup; the trial is $0 today and auto-bills at trial end if not cancelled. Applies to both email and Google OAuth signups.
New POST /billing/checkout-session backend endpoint (JWT-derived tenantId only — IDOR-safe). Idempotency guard returns { alreadySubscribed: true } if an active subscription already exists. Creates a Stripe Embedded Checkout session with payment_method_collection: 'always' and 30-day trial.
DEFAULT_TRIAL_DAYS env var (default 30) controls trial duration across all subscription creation paths.
Frontend polls refreshMe (10 × 2s) after Stripe's onComplete fires to confirm webhook delivery before advancing.
Orphaned-tenant guard: tenants with signupFlowComplete=true but no subscription are redirected to /onboarding?step=5 instead of the dashboard (owner + admin only — team members pass through).
@stripe/stripe-js upgraded from 1.54.1 → 9.1.0 (createEmbeddedCheckoutPage requires v9+).
Step indicator updated to 6 dots; team invites moved to Step 6; processing screen to Step 7.
Founder Cockpit internal dashboard (GCP staging only) — new /founder-cockpit page gated behind showFounderCockpit environment flag (true only on GCP staging) AND ManagePlatformSettings permission (drew@upmeals.ca only). Displays: 4 KPI cards (pipeline value, overdue next steps, meetings booked 7d/30d, meetings completed 7d/30d with period toggle), pipeline stage bar chart by deal stage, revenue forecast line chart (weighted vs best case), closed-lost reasons donut chart (last 90 days), and Founder Review Queue task list (pending [Demi OS Review] HubSpot tasks). All data sourced from the Sales OS control plane (GET /hubspot/cards/founder-cockpit/dashboard + GET /hubspot/cards/founder-cockpit). Falls back to stub data while real control plane endpoints are deployed. Refresh button re-fetches all data without skeleton flicker. 14-test spec file covers data loading, chart building, tab switching, and trackBy helpers.
HubSpot Founder Cockpit dashboard — self-contained HTML dashboard at GET /cockpit on the Sales OS control plane. KPI tiles: active pipeline total, overdue next-steps count, meetings booked/completed (7-day and 30-day). Pipeline-by-stage bar chart and 90-day revenue forecast line chart via Chart.js. Closed-lost reason breakdown donut. Pending [Demi OS Review] task table. Token-gated via Bearer auth with session-storage persistence. Server-side metrics cache (5-minute TTL) with in-flight Promise deduplication to prevent concurrent requests from each firing independent HubSpot API calls and triggering 429 rate limits. Stale-while-revalidate fallback if HubSpot is temporarily unavailable. GET /cockpit/api/metrics JSON endpoint for programmatic access.
HubSpot inbound deal router workflow script — scripts/hubspot-sales-os/setup-hubspot-inbound-workflow.mjs creates or updates a HubSpot PLATFORM_FLOW workflow ("Demi Inbound Deal Router") that calls the control plane's /hubspot/workflow-webhook/inbound-routing webhook when a deal is created. Supports --dry-run (default), --apply, and --status modes. Handles create vs. update idempotently via PUT full-replacement.
Cloud Scheduler inbound routing scan — added to scripts/hubspot-sales-os/setup-cloud-scheduler.mjs as a fallback polling job (every 2 minutes, 4-minute lookback) for environments where the HubSpot Workflow trigger is not yet configured. retryCount: 0 prevents cascading queue backups given the 2-min schedule interval; Firestore idempotency prevents duplicate deal routing between poll cycles.
PO bulk actions toolbar — select up to 10 purchase orders from the list to Mark as Sent, Mark as Received (auto-fills received = ordered quantity for clean deliveries), or Cancel POs in one pass. Reuses the <um-bulk-action-toolbar> component used by orders/recipes/ingredients management. Pre-flight validation blocks the action and shows a specific error when POs have missing expected delivery dates (Mark as Sent) or are in an incompatible status (Mark as Received). Partial-failure responses are surfaced per-PO. Max 10 POs per bulk action enforced at both frontend and backend. Three new backend endpoints: POST /purchase-orders/bulk-submit, POST /purchase-orders/bulk-mark-received, POST /purchase-orders/bulk-cancel — each with per-PO transaction isolation and audit logging.
AI Feedback Browser admin tab UI (PR 5 of 7) — new ai-feedback component under Settings > AI Feedback tab (gated by ManagePlatformSettings). KPI row: Total Votes, Up:Down Ratio, Top Rejected Type, Most Active Tenant. Filter row: um-date-range-selector (default last 30 days), tenant and insight-type um-filter-dropdown, sentiment toggle, debounced free-text search. Paginated table via um-paginated-table with sentiment pills, recipe name links, Details button, and point-in-time tooltip. Async CSV/XLSX export: POST job → poll → open GCS signed URL. um-skeleton-text/um-skeleton-button during load; um-empty-state for zero-results. (PR 5 of 7 — requires PR 6 backend endpoints to be deployed first.)
Admin backend for AI insight feedback browser (PR 6 of 7) — new AiInsightFeedbackAdminService with cross-tenant findForAdmin (paginated, free-text q LIKE search with %_\\ escaping, date/sentiment/insightType/userId/tenantId filters), getSummary (total/up/down counts, upRatioPct, per-type breakdown, top-10 tenants), createExportJob (dispatches Cloud Task with 1500ms timeout; returns {batchJobId} immediately), and processExportJob (atomic Pending → Processing transition, GCS upload, 24h signed URL, retry-safe). Extends AiUsageAdminController with four new endpoints under /admin/ai-usage/feedback. Adds POST /internal/process-ai-feedback-export to InternalAiFeedbackController. BatchJob.type enum extended with ai-feedback-export-csv / ai-feedback-export-xlsx. All reads use bypassTenantIsolation: true. 26 new unit tests. (PR 6 of 7 in the AI insights hardening initiative.)
Extended accept/reject insight payloads with full feedback context (PR 4 of 7) — InsightFeedbackPayload interface defined in product-insights-api.service.ts with idempotencyKey (UUIDv4, crypto.randomUUID()), insightType, recipeId, recipeName, sourceIngredientId/Name, targetIngredientId/Name, costDeltaCents. Both acceptInsight and rejectInsight updated to the typed interface. Panel component builds payloads via buildBaseFeedbackPayload() helper; sentinel 0 ingredient IDs excluded. ai-insights.component.ts updated in parallel. Backward-compat value/impact fields preserved for ai_preference_memory. (PR 4 of 7 in the AI insights hardening initiative.)
Performance
GET /recipes/:id/costing-data — 24s → ~2s cold-cache latency fix: Two bugs in CostingService.calculateRecipeCostPer100UnitsRecursive() caused every request to recalculate all sub-recipe costs from scratch in serial. Bug 1: the Redis cache read/write guard was level === 0 only, so sub-recipe costs (levels 1+) were never read from or written to Redis — each request started fresh regardless of cache state. Bug 2: the sub-recipe loop was for...of await (serial), so 3 sub-recipes at ~8s each cost 24s worst-case. Fixes: (1) remove the level === 0 Redis guard — all recursion levels now read/write Redis; (2) replace the serial loop with Promise.all + pLimit(3) for bounded parallel resolution (fresh pLimit instance per invocation to prevent parent/child deadlock); (3) request-scoped memo (Map<string, Promise<CostingInfo>>) deduplicates identical sub-recipe calculations when the same recipe appears in multiple branches of the tree (DataLoader pattern — promise registered synchronously before first await to prevent concurrent siblings racing past the has() check); (4) undefined recipeInstance.id guard prevents recipe:undefined:... cache-key collisions on unsaved draft recipes; (5) Redis read failures treated as non-fatal cache misses rather than propagating as 500 errors. Observed improvement on Recipe 4462 (Almond Chicken Bites): 24s → ~2s cold cache.
Changed
Trial duration 14 → 30 days — all subscription creation paths (createSubscription, createDefaultSubscriptionForCurrentTenant, new checkout session endpoint) now default to 30-day trials via DEFAULT_TRIAL_DAYS env var.
Pricing tier copy — "14-day free trial" updated to "30-day free trial"; "No credit card required" updated to "Cancel anytime before trial ends" to reflect the new CC-required flow.
Signup page subtitle updated to "Start your 30-day free trial".
POST /purchase-orders/{id}/submit — breaking API contract change: Response status changed from 204 No Content (void) to 200 OK with the full updated PurchaseOrder entity. The submit endpoint is now hardened with a compare-and-swap (updateAll predicate + exclusive InnoDB row lock) to prevent double-submit races; cross-tenant PO IDs surface as 404 (previously could surface as 409); 409 Conflict is now returned instead of 400 Bad Request when the PO is not in Draft status. Any caller that checks for 204 or discards the response body must be updated.
TenantCurrencyService + UmCurrencyPipe — fix currency inconsistency on inventory pages:edit-ingredient.component.ts was formatting procurement costs as en-US / USD; purchase-order-edit.component.ts was using Angular's CurrencyPipe with a hardcoded 'CAD' constant. Both are now replaced by a new TenantCurrencyService (Angular Signal + static constants DEFAULT_CURRENCY = 'CAD', DEFAULT_LOCALE = 'en-CA', DEFAULT_DIGITS_INFO = '1.2-2') and a new UmCurrencyPipe (| umCurrency: tenantCurrencyService.currencyCode()). en-CA locale registered in app.module.ts. Pipe returns null for null / undefined / '' / NaN; zero formats as $0.00. Currency code is an explicit pipe argument (not read from the Signal inside transform()) so Angular CD tracks it correctly. SaaS billing pages (enterprise-dashboard, promo-codes) intentionally retain USD and are not affected.
PO detail/edit consolidation (PR 4 of PO bug sweep, Bugs 2, 4, 7): Deleted the standalone PurchaseOrderDetailComponent and flipped the /inventory/purchase-orders/:id route to PurchaseOrderListComponent, which opens the existing edit modal in readonly mode. Readonly view now includes Vendor Code and Unit columns on the line-items table (populated from backend-denormalized vendorProductCode / packDescription fields). In Submitted / Partially Received views, the Received column sits immediately beside Order Qty for easy visual comparison. Overflow header actions (Download PDF / Download Excel / Print / Duplicate / Cancel) are consolidated into a single 3-dots menu matching the order-detail / recipe-detail idiom. Edit is a prominent footer button on draft POs. Preview PDF has been removed — Download PDF covers the use case. Catalog fetch is gated behind !readonly to skip unnecessary HTTP requests on view-only loads. The Received column and per-row line-item Status badge are hidden on draft POs. Clicking Mark as Sent without an expected delivery date fires a concise toast instead of blocking with a disabled button. The "Demi won't email your supplier" message is consolidated into a single tooltip on the Mark as Sent button. Download PDF and Download Excel use the existing async Cloud Tasks + GCS pipeline (exportBatch + polling) with a new single-file bypass in BatchExportService that skips ZIP overhead for single-PO exports. Pure PoLineItemStatusPipe added to replace the previous method-in-template anti-pattern for line-item status badges.
Cloud Tasks write path for AI insight feedback (PR 3 of 7) — every thumbs-up / thumbs-down vote on an AI insight now dispatches a Cloud Task to POST /internal/record-ai-feedback, which writes an append-only audit row to ai_insight_feedback with full point-in-time user + recipe + swap context. The dispatch is awaited with a 1500ms timeout and swallowed to Sentry on failure so the user vote always returns 204; at-least-once deliveries collapse onto the UNIQUE(idempotency_key) constraint from PR 2. Ships with an FF_FEEDBACK_CAPTURE kill-switch (dispatch AND worker both short-circuit) and a GDPR anonymizeUser stub. Requires gcloud tasks queues create ai-feedback-queue --location=northamerica-northeast1 --max-attempts=5 --max-backoff=300s --min-backoff=10s in each environment before first deploy. Unblocks PRs 4–7.
Cloud Scheduler retention purge for AI insight feedback (PR 7 of 7) — daily POST /internal/purge-ai-feedback endpoint deletes ai_insight_feedback rows older than FF_FEEDBACK_RETENTION_DAYS (default 365) and stale ai-feedback-export-*BatchJob rows older than FF_FEEDBACK_EXPORT_BATCH_JOB_RETENTION_DAYS (default 30). OIDC-authenticated (Cloud Run IAM + inline google-auth-library iss/aud/email verify), cross-tenant, idempotent, timezone-safe UTC math. Failures log at error level and re-throw so Cloud Scheduler retries per job policy. Requires GCP provisioning before first deploy — see .claude/context/DEPLOYMENT-GUIDE.md for the gcloud scheduler jobs create command.
ai_insight_feedback MySQL audit table for AI insight thumbs-up / thumbs-down votes — new AiInsightFeedback model + AiInsightFeedbackRepository landing the schema for the AI Insights hardening initiative. Append-only row per vote with denormalized point-in-time user / recipe / swap context (user email + display name, recipe id + name, source and target ingredient ids + names, signed cost_delta_cents, sentiment enum 'up' | 'down'). idempotency_key varchar(36) NOT NULL UNIQUE guarantees at-least-once-delivery safety for the Cloud Tasks worker that will land in the next PR — the write path will catch ER_DUP_ENTRY silently so retry storms produce exactly one row per vote. reason_chips json and comment text columns are reserved for a future expansion so the schema is forward-compatible without a follow-up ALTER TABLE. Five composite indexes cover the admin browser query patterns: primary (tenantId, created_on), sentiment-filtered (tenantId, sentiment, created_on), cross-tenant analytics (insight_type, created_on), and per-user audit / GDPR anonymization (user_id, created_on). A post-schema migration at backend/src/scripts/migrate-ai-insight-feedback.ts defensively re-applies the UNIQUE constraint and every composite index on every deploy (idempotent — re-running is a no-op when the indexes are already present) so drift from LoopBack's alter-mode migrateSchema can't leave the table unindexed. (PR 2 of 7 in the AI insights hardening initiative — schema-only, zero controller wiring.)
Changed
Staging MongoDB Atlas cluster migrated from AWS ca-central-1 to GCP northamerica-northeast1, eliminating the cross-cloud latency hop that staging Cloud Run was paying on every query; production cluster untouched. New cross-cloud Atlas region migration runbook added to docs/infrastructure/mongodb-atlas.md (referenced by P28 in the GCP cutover runbook) for the eventual prod migration.
Fixed
"Create PO" button no-op — GenericManagementService.processItemAction() switch had no case 'create' handler, so createOrder() fired itemAction({ action: 'create' }) and returned silently without opening the modal. Added case 'create': return this.addItem(); so the create flow is correctly routed. Found during QA of the PO modal refactor.
SweetAlert dialogs fail to render after a preConfirm dialog — defaultAlertOptions in alerts.component.ts was setting willOpen, didOpen, willClose, didClose, didRender, and didDestroy to undefined. The ngx-sweetalert2 SwalComponent exposes these as @Output() EventEmitters; the swalOptions setter calls Object.assign(this, options) which overwrote those EventEmitters with undefined, causing TypeError: Cannot read properties of undefined (reading 'emit') and preventing any subsequent popup from rendering. Removed these six keys from the defaults (they must never appear there) and added a JSDoc block explaining why.
HubSpot Founder Cockpit "Load failed" error from HubSpot 429 rate limiting — concurrent requests to /cockpit/api/metrics each fired independent HubSpot API calls in parallel, saturating the rate limit and causing the dashboard to show "Load failed". Fixed with in-flight Promise deduplication (cockpitMetricsFetchInFlight Map): concurrent requests for the same pipelineId now share a single in-progress fetch. Cache TTL increased from 60s to 300s (5 minutes) to further reduce HubSpot API call frequency. Error message in the dashboard now includes the specific error detail instead of the generic "Load failed — will retry".
HubSpot Sales OS control plane TypeScript errors — fixed 23 pre-existing TypeScript strict-mode violations in server.ts: added HubSpotApiError interface (extends Error with .data and .status fields), typed evaluateDealRisk and modelDealValue parameters as Record<string, unknown>, narrowed reason instanceof Error in unhandledRejection handler, cast rampSkipsByAction reduce accumulator, added union discriminant cast for validation .reason field. All tsc --noEmit errors resolved.
HubSpot card unit tests failing with jest is not defined — DemiSalesOsCard.test.tsx used jest.mock/jest.fn() APIs in a Vitest project. Replaced all jest.* calls with vi.* equivalents. Fixed temporal deadzone crash (Cannot access 'DemiSalesOsCard' before initialization) caused by the extend mock immediately invoking its callback on import before the component const was initialized — extend is now a no-op vi.fn().
evaluateDealRisk callers referencing non-existent .level and .reasons fields — two card action endpoints merged from main referenced riskEval.level and riskEval.reasons (plural array), neither of which exist on the return type. The function returns { risk, reason } (singular string). Both call sites corrected to use riskEval.risk and riskEval.reason.
PO list rows now navigate to detail on click + Cmd/Ctrl+Click opens new tab — <um-paginated-table>'s row-click behavior is now opt-in via a new [rowClickable] Input. Setting it to true adds the cursor-pointer affordance, emits rowClick with a { item, event } payload, and gates role="button" + tabindex="0" on actually-loaded rows so screen readers no longer announce skeleton placeholders as buttons. Critically, the previous behavior unconditionally wired every row in every table (44+ consumers) as a keyboard-focusable button with no handler, a latent WCAG violation that this PR fixes app-wide. Only ai-insights and the new PO list opt in. PO list viewOrder() repointed to navigate to the existing detail route so the row-click and 3-dots "View" action converge on the same destination. (PR 5 of the post-first-gen PO bug sweep, Bug 1.)
Editing an existing PO no longer fires the unsaved-changes guard on load — purchase-order-edit.component.ts previously enriched lineItems$ with stockStatus/priceChangePercent/etc. fields AFTER resetInitialState() had captured the dirty baseline; the differ saw those phantom mutations and flagged the form as dirty before the user touched anything. Display values are now computed via two new pure pipes (StockStatusPipe, PriceChangePipe) in the template, so lineItems$ stays immutable after load. The vendor-catalog lookup maps (stockMap, parMap, trackedSet, lastPoCostMap) are now reassigned to fresh instances on every catalog load instead of being mutated in place, satisfying the pure-pipe reference-stability contract. (PR 5, Bug 3.)
SweetAlert callback leak between sequential dialogs — preConfirm, preDeny, inputValidator, willOpen, didOpen, willClose, didClose, didRender, and didDestroy callbacks no longer leak from one alert into the next. ngx-sweetalert2's SwalComponent retains every touched Input via an internal touchedProps Set; an Input that's missing from the next caller's options is silently re-emitted with its previous value. The shared defaultAlertOptions map now explicitly resets every callback / lifecycle Input with undefined, with a JSDoc rule documenting the convention so future engineers don't reintroduce the leak. (PR 5, Bug 6.)
PO detail "Submit Order" button renamed to "Mark as Sent" with confirmation modal — clarifies that Demi does not email the supplier directly. New helper text under the button (Demi won't email your supplier), and a confirmation modal with the copy "This flags PO-{N} as sent to {vendor} so it counts toward inventory tracking. You still need to email or call your supplier separately. Demi does not send the order automatically." The modal uses SweetAlert's preConfirm + showLoaderOnConfirm so the loading spinner stays visible during the submit network call (prevents double-submit) and allowOutsideClick: false so users can't dismiss mid-flight after the backend has already committed. Success toast: "Marked as sent. Remember to send the PO to {vendor} directly." (PR 5, Bug 8.)
PO list "Delete" action returned 404 "API not found !" — the row-action menu on /inventory/purchase-orders was calling DELETE /api/purchase-orders/{id}, which the backend deliberately does not expose (purchase orders are cancellable, not deletable, to preserve the audit trail for regulatory compliance and vendor-invoice reconciliation). Renamed the action to Cancel (ban icon, still styled text-danger) and rewired cancelOrder() to POST /purchase-orders/{id}/cancel — the same endpoint the controller already ships. Confirmation modal copy is deliberately minimal: *"PO-XXXXX will be cancelled. This action cannot be undone."* Pre-existing bug — surfaced on 2026-04-08 during the manual Step 0 cleanup for PR #1165 when attempting to remove the 4 bad-quantity draft POs on Vancouver staging; not caused by PR #1165 (git diff f5619a943..b9ab4d160 only touched one test-fixture file in the PO service test suite). The PO edit modal also gains a matching 3-dots dropdown in the header (mirrors the edit-order.component.html dots-horizontal pattern) with a single Cancel purchase order item gated to existing drafts — operators can now cancel a draft PO from inside the open modal without closing it first, and on success the modal emits saved({exit: true}) so the parent list reloads and the modal closes in one pass. Both entry points share byte-identical title / body / button copy and call the same poApi.cancel(poId) endpoint. Added 12 new unit tests total — 4 on the list (modal copy, happy path, failure branch, tableActions shape) and 8 on the edit modal (canCancelOrder gating across 4 PO states, modal copy, no-op on unsaved PO, happy-path emits saved(exit:true), failure branch keeps the modal open).
Changed
Flattened nested subscribe in purchase-order-edit.loadVendorCatalog — pre-existing getWithLineItems subscribe nested inside the forkJoinnext callback is now flattened with switchMap, with explicit catchError handling that falls back to an empty lastPoCostMap if the last-PO lookup fails. The catalog itself is still usable without the price-change badges, the UI never hangs on a half-loaded state, and the observable lifecycle is managed in one chain. (PR 5 cleanup, surfaced by Gemini CTO review.)
PO auto-generate soft-degrades instead of halting when some ingredients have bad procurement data (Bug 5 — PR 2 of the PO bug sweep) — computeDeficitsWithProcurement() now collects ingredients with invalid procurement into an invalidDeficits list instead of halting the entire run. Generation continues with the remaining valid ingredients; the operator sees a persistent warning toast naming the excluded rows (up to 5 inline, "+N more" suffix) and can fix them in Inventory Management before regenerating. A complete halt is still triggered when ALL deficit ingredients are invalid — there is nothing left to generate. New 'unit-conversion-failed' reason code surfaces the case where a recipe uses one measurement class (e.g. grams) while the inventory unit is in an incompatible class (e.g. litres); silently using that demand would inflate the order quantity with values in the wrong unit. getQtyPerCase in the unit-conversion utility now distinguishes null/undefined (2-tier procurement — treat as 1) from explicit 0 or negative (corrupted data — return null so callers surface it instead of silently dividing by zero). FLOAT_TOLERANCE = 1e-9 constant applied to both Math.ceil(suggestedCases - FLOAT_TOLERANCE) call sites so IEEE 754 float division (e.g. 100 / 25 = 4.0000000000001) cannot trigger an off-by-one extra case order. Math.max(0, Math.ceil(...)) added as a guard against the degenerate suggestedCases < FLOAT_TOLERANCE case. Frontend warning toast message is intentionally generic (not "incompatible unit configurations") because the invalidDeficits array can contain any InvalidDeficitReason (e.g. invalid-case-qty, null-shelf-unit) depending on what the procurement gate caught. Backend unit-conversion utility gains full unit-test coverage: FLOAT_TOLERANCE value + float-imprecision guard, all four conversion functions with 2-tier/3-tier/null/zero/negative inputs. (PR 2 of the post-first-gen PO bug sweep.)
Inventory tracking wizard now blocks corrupt procurement data upstream — extended the shared validateProcurement() validator with 4 new structural checks (invalid-case-qty, null-shelf-unit, invalid-shelf-amount, missing-conversion-weight) covering the cucumber/parsley failure mode that produced 471 cases of parsley ($16.6K line) on Vancouver staging on 2026-04-08. The wizard's IngredientInventoryConfigController.batchUpdate now runs the full 8-check validator before opening any transaction; if ANY ingredient in the batch fails, the entire request is rejected atomically with a structured 422 Unprocessable Entity response carrying details: {code: 'TRACKING_VALIDATION_FAILED', failures: [{ingredientId, ingredientName, reasons}]}. The frontend wizard surfaces each failure inline with a "Fix this row" deep-link to the ingredient editor and an "Unselect" recovery button, blocking the "Try again" button until every bad row is resolved. New read-only GET /ingredient-inventory-config/audit-tracking-readiness endpoint runs the same validator across the calling tenant's currently-tracked ingredients and returns the list that would now fail the gate, so operators can preview the impact and clean up legacy data BEFORE the corresponding PO auto-generate soft-degrade ships. The audit endpoint resolves the effective inventoryUnitId via the same fallback chain batchUpdate uses, so legacy rows with inventoryUnitId = NULL in the DB still trip the missing-conversion-weight check instead of being falsely reported as healthy. Cloud Logging strips ingredientName from validation failure metadata to avoid leaking proprietary recipe terms across tenants — the structured 422 still carries names to the frontend so the operator sees them in the wizard. Backend gains 26 new unit tests covering each new failure mode, priority ordering, the cross-unit-class skip path, the wizard's atomic-rejection guarantee, and the audit endpoint's effective-inventoryUnitId resolution. Verified by Gemini 3.1 Pro CTO review (Vertex AI) before merge — caught 1 critical (audit-endpoint NULL-inventoryUnitId bypass) + 1 high (PII in logs). (PR 1 of the post-first-gen PO bug sweep.)
PO auto-generate failed with EntityNotFound for every vendor on GCP staging — PurchaseOrderService.createDraft() opens a per-vendor MySQL transaction at READ_COMMITTED isolation, creates the PO row inside the transaction, then immediately calls recalculateSubtotal → poRepo.updateById(..., txOptions). The audit-wrapped updateById (and the matching update / replaceById / deleteById / deleteByIdHard overrides) was doing its pre-fetch via findByIdIncludeSoftDelete(id) *without forwarding options*, so the read checked out a fresh connection from the pool that ran outside the transaction. Under READ_COMMITTED, the just-created PO row was invisible to that out-of-band read, the audit pre-fetch threw EntityNotFound, the transaction rolled back, and the per-vendor catch in createFromBelowPar flipped that vendor into failedVendors. Every vendor failed identically — the operator-facing toast read "Created 0 purchase order(s), but N vendor(s) failed". Fix: forward options to all 5 findByIdIncludeSoftDelete(id, options) call sites in default-auditable-entity.repository.ts so the audit pre-fetch joins the caller's transaction and sees the in-flight row. This is also a latent correctness improvement for any future "update twice in one transaction" path — the audit log was previously reading pre-transaction state and missing intermediate uncommitted changes. Verified by Gemini 3.1 Pro CTO review (Vertex AI) before merge.
Wizard procurement integrity helper extracted from PR #1154 — the 4-check classification loop in PurchaseOrderService.computeDeficitsWithProcurement is now a shared helper at backend/src/services/shared/procurement-integrity.ts (validateProcurement + isValidCost) so the upcoming inventory tracking wizard gate can call the same validator without drift. Behavior-preserving refactor — PO generate still routes no-procurement to the soft-skip path and the other 3 reasons to the fail-loud halt; existing 47 PO service tests are unchanged.
PO auto-generate still returned calculation-failed post-PR #1150 — root cause was two classes of broken procurement data in staging: (1) IngredientProcurement rows with NULL purchaseUnitId that passed the old proc && proc.vendorId filter, dropped null into the PurchaseOrderLineItem payload, and failed LB4 model validation inside the transaction; (2) vendors in an inconsistent soft-delete state (deleted=0 AND deleted_on IS NOT NULL) that vendorRepo.findById filtered out, causing EntityNotFound inside createDraft(). computeDeficitsWithProcurement() now pre-validates every deficit ingredient's procurement row — preloading all referenced vendors in one tenant-scoped vendorRepo.find and classifying each row against three hard integrity checks (null-purchase-unit, invalid-cost, unreadable-vendor). The cost check is NaN-safe (!(Number(x) > 0)) so undefined/null costs are rejected instead of bypassed. Deficits missing a default vendor entirely are soft-skipped (not halted) so one unconfigured ingredient cannot paralyze an entire facility's supply chain — the legacy MissingProcurement reason still fires when *every* deficit lacks procurement. If any HARD integrity failure is present, generation is halted entirely (no partial success — a silently dropped "tomatoes" ships kitchens short) and a new InvalidProcurementData reason code is returned with an invalidDeficits list naming every broken ingredient; the frontend shows a persistent (no auto-dismiss) warning toast listing the first 5 names semicolon-separated (so names like "Spice, Sumac" remain readable) with a "+N more" suffix, so the operator can fix every bad row in one pass and re-run. A new PartialSuccess reason code surfaces the case where some vendor POs were created but others threw unexpected runtime errors mid-loop — the frontend warns the operator by naming each failed vendor (so they can retry selectively) instead of silently dropping them. Per-vendor createDraft catch logs now include tenant + ingredient context AND the full stack trace so GCP Error Reporting can group by root cause.
Inventory Print / Export PDF returning 500 on staging — PDF service image was built before the /generate-inventory-pdf route landed, causing 404 Cannot POST wrapped as 500. Rebuilt from main. Backend now returns 503 ServiceUnavailable with a user-friendly message when the PDF microservice fails (matching the sibling 503 branch and the frontend's existing 503 handler). PDF service /health now returns the build commit SHA for one-curl version-drift diagnosis, mounted routes are introspected on boot via express-list-endpoints and logged as structured JSON, and cloudbuild.yaml region is no longer hardcoded to the wrong value. Same 503 + structured-logging pattern applied to the sibling PhysicalCountExportService and PurchaseOrderExportService for consistency.
Generate Purchase Orders always returned empty on staging — root cause was deliveryDate: {inq: [UTC-midnight Date, ...]} in the auto-generate flow, which never matched MySQL DATETIME rows that had any time component (a Vancouver order delivered 3 PM PDT is stored as 2026-04-12 22:00:00 UTC, not 2026-04-12 00:00:00). Replaced with a timezone-correct between range computed in the sub-tenant's local timezone, dropped the Execution-production-day DOW intersection so every calendar day in the window contributes, chunked the order fetch (500/page with setImmediate yield) to prevent Cloud Run OOM at scale, and applied symmetric defense-in-depth tenant scoping to every downstream repo call. KPL override now compares in the sub-tenant local day (same root-cause class the main query had). Demand calculation is wrapped in try/catch with structured error logging and a new CalculationFailed reason code surfaced as a red error toast, so Cloud SQL hiccups no longer masquerade as a success state. Auto-generate now returns a typed response with a specific reason code (Success, NoTrackedConfigs, NoOrdersInWindow, NoTrackedIngredientDemand, NoDeficit, MissingProcurement, AllAlreadyInOpenPos, CalculationFailed), each mapped to a severity-correct toast explaining exactly what the user needs to do next.
Inventory export returning 403 Forbidden — stock levels PDF and Excel exports now reach the correct sub-tenant. The export call was building HTTP headers manually and omitted x-tenant-id, x-sub-tenant-id, x-tenant-subdomain, and Accept-Language, so the backend resolved to the wrong sub-tenant and rejected the request. Now uses the shared ApiService.getTenantHeaders() helper so raw blob calls carry the full tenant context like every other API call.
AI Usage CSV export missing icon — dropdown referenced non-existent file-03.svg; now uses file-download-03.svg.
AI Usage PDF export was a raw print screen — replaced window.print() with proper jsPDF-generated PDF featuring Demi branding, KPI summary cards, styled data tables with teal headers, page-break logic to prevent row truncation, and a confidential footer.
AI Usage Excel export had zero formatting — applied the same branded styling used in Recipe/Ingredient exports: title section with Demi branding, blue column headers, bordered data rows, currency formatting, and period footer across all three sheets.
AI Usage CSV not machine-readable — restructured CSV for AI/ML processing with # comment metadata, snake_case headers, raw numeric values (no $ or commas), and three flat RFC 4180 sections.
PAR unit dropdown missing scaled units — Litre (for volume) and Kilogram (for weight) now appear in PAR unit dropdowns across the inventory dashboard bulk edit, ingredient detail modal, and setup wizard. Fixed measurement base unit passed as string instead of proper object in two of three callers.
PAR unit auto-conversion — switching between compatible measurement units (e.g., ml↔L, g↔kg) now auto-converts the numeric value instantly. Switching to incompatible units (e.g., ml→Bottle) resets to empty for manual entry.
Ingredient detail: current inventory field — tracked ingredients now show actual on-hand stock below PAR level. Untracked ingredients show a "Set up tracking" CTA that opens the wizard at step 2 with the ingredient pre-selected.
"Add to inventory tracking" link color — changed from Bootstrap blue to Demi brand teal.
PO auto-generate "sufficiently stocked" false positive — replaced vendor-level PO dedup (skipped ALL items for a vendor with any draft PO) with ingredient-level filtering, matching the reorder recommendations logic.
Export error handling — PDF export now returns 503 with "use Excel" message when PDF service is unavailable. Frontend shows status-specific error messages (403 permission, 503 unavailable, 422 validation).
Proactive insights cron job Redis crash — the daily insight warm-up cron was processing ALL active tenants with no rate limiting, overwhelming Redis on staging (641s run, cascading timeouts). Now filters to only Growth/Trial subscribers with AI Insights access, uses p-limit(1) sequential processing with 30s per-recipe timeout, 5min global job timeout, and per-tenant recipe caps. Removed uncontrolled fire-and-forget Tier 2 bulk warming entirely.
AI insights listing page hangs on Redis saturation — added 3-second timeout on Redis bulk read (mget). When Redis is slow or down, the page returns partial results with an info banner instead of hanging indefinitely.
SIGTERM graceful abort for Cloud Run — cron job now checks for SIGTERM between recipes and aborts gracefully with partial results logged, preventing hard termination during Cloud Run scale-in.
Added
PAR level on ingredient detail modal — ingredient edit modal now shows the PAR level field with a unit dropdown matching the wizard's procurement-based unit selection (Case, Bottle, Litre, etc.). Editable in edit mode, read-only display when viewing. Saves via batchUpdate() with automatic shelf-unit conversion.
Batch inline PAR editing on dashboard — new "Edit PARs" toolbar button toggles all PAR cells in the Stock Levels table into editable inputs. Global Save/Cancel with loading spinner and change count. Single batchUpdate() call for all changes.
Manual stock adjustment modal — new "Adjust stock" option in the 3-dot menu on Stock Levels table. Opens a modal showing current stock, new level input with auto-calculated delta (+/- with color), optional reason dropdown (Waste, Theft, Spoilage, etc.), and optional notes field.
PO row selection with batch export — checkboxes on Purchase Orders list following the Invoices SelectionManagerService pattern. Selection-aware Print and Export buttons showing count (e.g., "Export (3)").
Delete option for cancelled physical counts — cancelled counts can now be deleted via the 3-dot menu. Backend uses transactional soft-delete (parent + child items atomically) with proper validation (only cancelled, not already deleted).
Changed
PO header restructured — separated action buttons (Print, Export, Create PO) from auto-generation controls. Date selector + Generate button now grouped with a label and subtle background for clear information hierarchy.
"Add ingredients" renamed to "Manage tracked ingredients" to reflect that the wizard handles both adding and removing ingredients from tracking.
Reset button hidden when no filters active — the shared paginated-typesense-table Reset button now only appears when there are active filters or search terms, reducing visual clutter across all pages using this component.
Auto-scale ml/g display to L/kg — large shelf-unit amounts in base measurement units are now auto-converted to human-readable scaled units (e.g., "5460 Milliliters" → "5.46 Litres"). Uses pre-computed scale map for O(1) per-row lookups.
Physical count item constants extracted — PHYSICAL_COUNT_ITEM_FIELDS_WITHOUT_NOTES constant moved from duplicated inline definitions to a shared constants file.
Fixed
AWS prod export bridge security + CI typing — internal batch OIDC verification now requires explicit API_URL audience binding instead of request-derived host fallback, and the Cloud Tasks bridge client cache reset now uses the correct TypeScript type-safe retry path.
Duplicate dirty guard on PO draft — removed redundant SweetAlert from cancel() method. The global UnsavedChangesGuard now handles all dirty-form navigation consistently with a single Save/Discard dialog.
PAR level 404 on ingredient detail modal — fixed endpoint mismatch where getById() called a non-existent GET /ingredient-inventory-config/{id} route. Now uses getItem() with an ingredientId filter against the existing list endpoint.
Silent 409 on duplicate physical count start — fixed error message extraction using getErrorMessage() helper instead of incorrect nested path. Added isStartingCount flag to disable the button during the API call, preventing double-clicks.
SSE Streaming Loading State for AI Insights — replaced static "Analyzing your recipe" loading text with real-time SSE-triggered rotating stage messages. Uses fetch() with Bearer auth (EventSource doesn't support custom headers), with automatic fallback to POST if streaming fails. Animated dots suffix, insight counter with fade-in, and NgZone.run() wrapper for proper change detection.
Background Tasks in Topbar — new hourglass icon with red badge count appears when background tasks are running. Opens a 380px side panel showing task cards with progress bars, status tints (running/complete/error), cancel with confirmation modal, and auto-dismiss after 5 minutes. Polling-based with smart start/stop lifecycle.
AI Usage Admin Dashboard — new "AI Usage" tab in Settings showing cross-tenant KPI summary cards (total spend, requests, avg cost, tokens) with trend delta indicators vs prior period, date range selector with 24h/7d/30d/90d presets + custom picker, spend-by-feature and spend-by-tenant tables (um-paginated-table), and CSV/Excel/PDF export. Backed by new GET /admin/ai-usage endpoint with cross-tenant SQL aggregation via getAdminUsageStats() (parallel Promise.all queries, MySQL BIGINT-safe parsing).
AI Insight Feedback Buttons — thumbs up/down buttons on each AI insight card in the recipe insights panel. Positive feedback calls POST /insights/accept to reinforce AI memory; negative feedback calls POST /insights/reject to adjust future suggestions. Three visual states: default (neutral), accepted (green), rejected (red). One-way per-insight feedback with optimistic UI + state rollback on API error.
Cross-Recipe AI Insights Listing Page — new /ai-insights route showing all recipes with pending AI suggestions in a sortable, searchable table. Accordion rows expand to reveal compact insight cards with inline accept/dismiss actions and feedback buttons. Running session savings counter tracks progress. Server-side pagination for enterprise scale. Bulk-accept endpoint for batch operations. All 3 subscription gating layers (route guard, sidebar visibility, backend 403). ARIA attributes and keyboard navigation on accordion rows. Optimistic updates with automatic rollback on API failure.
Changed
IndexedDB cache: Added 24h soft TTL — entries older than 24 hours are treated as cache misses with async cleanup of expired entries
Cache security: Added cross-tenant IDB write guard in GenericStoreService — prevents background refresh from writing Tenant A data after switching to Tenant B
AI savings report tests: Fixed timezone bug (local vs UTC dates), corrected assertions to match Phase B3 template schema (17 variables), added zero-savings edge case
Cache tests: Added IndexedDB cascade, SSE invalidation, and model filtering tests to GenericStoreService
Inventory module UX polish (design review) — unified header spacing (mb-xl → mb-3xl) across Ingredients, Purchase Orders, and Physical Counts pages to eliminate vertical content jump on navigation. Added hover feedback on clickable KPI cards and vendor accordion headers ($bg-secondary with 150ms transition). Moved look-ahead days selector from PO header to filter bar to reduce header clutter (4 → 3 controls). Always render 5th KPI card ("Est. Reorder Cost") to prevent grid layout shift. Added empty state for Physical Counts page with description and CTA button. Replaced inline style with component class, non-standard table-header-label with utility classes, and added aria-label on dismiss recommendation button for accessibility.
Fixed
AI Gateway GCP auth — AiGatewayService now attaches OIDC id-token headers on all Cloud Run requests, fixing HTTP 403 errors on staging AI insights endpoints. Moved getAuthHeaders call inside the aiGateway feature flag check so pass-through requests (when flag is disabled) stay clean. Added comprehensive unit tests for post/get auth paths and feature flag behavior.
ObjectId serialization in MySQL tenant scoping — applyCustomTenancyContext() in BaseMultiTenantRepository passed raw MongoDB ObjectId objects into MySQL WHERE clauses without .toString() conversion. LoopBack's MySQL connector cannot match ObjectId objects against string columns, causing all queries with customTenancyContext to return empty results ([]). Affects 8 controllers using getTenantScopedOptions(). Fixed by adding defensive .toString() ?? null at the single choke point where all custom tenancy context values enter the query pipeline.
Multi-tenant middleware ObjectId comparison failure — tenant context middleware used template literal interpolation to compare MongoDB ObjectIds with string IDs from headers/JWT. This does not reliably produce clean hex strings, causing sub-tenant resolution to fail silently. Fixed all 7 occurrences with String(t.id) === String(id). Also added JWT subTenantIds (plural array) fallback.
Inventory wizard data not persisting (Typesense tenant isolation leak) — syncToTypeSense() passed bypassTenantIsolation: true to toTypeSenseDocument(), which propagated to inventoryConfigRepo.findOne(). This caused the inventory config lookup to query without tenant scoping, returning null and setting isInventoryTracked=false in Typesense documents. The inventory dashboard filters on isInventoryTracked:true, so saved ingredients never appeared. Fix enforces tenant isolation inside toTypeSenseDocument() for inventory config and stock queries.
Undelete Endpoint Tenant Authorization (TS-689) — all 23 undeleteById endpoints now enforce tenant isolation. Previously, these endpoints only checked role permissions via @authorize but never verified the entity belonged to the requesting user's tenant, allowing cross-tenant entity restoration by ID guessing. Added authorizeMultiTenantUndelete() helper function and applied it across all multi-tenant controllers, with admin-only gates for customer/location entities and defense-in-depth checks for global entities.
Dashboard images not loading on GCP staging — resolved root cause where IndexedDB persistence layer in GenericStoreService ignores include filters, causing enrichProductImages() to receive products without the recipe relation from IDB cache. Added force=true to bypass IDB for queries requiring relation includes. Also added (error) fallback handler to um-image-display component so broken image URLs gracefully show placeholders instead of invisible broken icons.
Inventory dashboard skeleton loader incomplete — expanded the skeleton loader to cover the full page layout (KPI cards, page header, stock levels header, and 8-row table skeleton) instead of showing only 4 card outlines with blank space below.
Added
AI Savings Report Cron & Service — monthly cron job that generates and emails AI usage and cost summaries to tenant admins. Runs at 9am UTC on the 1st of each month, processes all active tenants in batches of 3, reports on request counts/costs/top features/top preferences, and includes resilient error handling with Sentry tracking. New AiSavingsReportService (singleton) handles computation and fire-and-forget email delivery.
Proactive Insights Cron Job — daily cron job (4am UTC) that pre-warms AI product insights for all active tenants. Tier 1: processes top 20 recipes per tenant in batches of 3 with 500ms breathers. Tier 2: dispatches remaining recipes to the Python AI microservice in sequential 50-recipe chunks to prevent DoS.
Hash-based change detection for AI insights — SHA-256 hash of recipe ID + sorted ingredient IDs with 24h Redis TTL. Detects whether a recipe's composition has changed since the last analysis, preventing redundant AI calls in cron pre-warming. Controlled by FF_HASH_DETECTION feature flag.
Purchase Order Bulk Line Item Updates — replaced individual line item mutation endpoints with a bulk updateDraftWithLineItems service and controller method. This allows patching an entire purchase order draft (header + all line items) in a single transactional request, simplifying the frontend UI and reducing network overhead. Tests have been added to verify functionality.
Purchase Order Modal Editing — refactored the frontend Purchase Order UI to use GenericManagementComponent and GenericEditComponent, enabling optimistic UI updates and editing drafts in a modal window rather than full page navigations.
Fixed
SSE authentication security — replaced JWT query parameter with a short-lived, single-use ticket system. The Angular client now calls POST /events/auth (Bearer header) to obtain a 30-second UUID ticket stored in Redis, then connects via GET /events/stream?ticket=<uuid>. Prevents JWT exposure in reverse-proxy and APM access logs. Reconnects automatically fetch a fresh ticket.
SSE permission staleness — added periodic 5-minute re-validation of sub-tenant permissions on active SSE connections. Queries UserTenantRepository to refresh the connection's subTenantIds scope; closes the connection immediately if the user's tenant access is fully revoked, so access revocations take effect before JWT expiry rather than at the next full reconnect.
AI insights circuit breaker false trips — AiStreamService now checks axios.isCancel() before recording circuit breaker failures. Browser tab closes/navigations no longer count as service failures, preventing 10 natural disconnects from taking the AI stream offline for the entire platform.
AI insights IDOR / cross-tenant data leak — ProductInsightsController.getInsightsStream now passes explicit { customTenancyContext: { tenantId } } when calling generateAiPayload, preventing tenant context from being stripped and allowing cross-tenant recipe ID access.
AI insights SSE stream corruption — getInsightsStream return type changed from Promise<void> to Promise<Response> with explicit return res. Prevents LoopBack from auto-sending a 204 response while the SSE stream is still writing, which caused ERR_HTTP_HEADERS_SENT crashes.
AI billing data truncation and OOM — AiUsageLogRepository.getUsageStats replaced in-memory aggregation (10k record limit) with native SQL SUM()/GROUP BY queries. Eliminates memory exhaustion and silently truncated billing data for high-volume enterprise tenants.
AI billing string concatenation bug — DECIMAL columns from MySQL connector are now aggregated in SQL, avoiding JavaScript += string concatenation that produced garbage values like "00.01" in email reports.
AI cron job empty tenant context — ProductInsightsService.getInsights and generateAiPayload now resolve tenantId from options.customTenancyContext.tenantId first, falling back to tenantHelper only for HTTP request context. Prevents cron jobs from sending empty tenant IDs to the Python AI service.
Python AI microservice DoS via unbounded batch — Tier 2 batch processing in ProactiveInsightsCronJob now chunks recipe IDs into 50-recipe sequential requests instead of one massive POST, preventing timeout/OOM on the Python service for large tenants.
AI savings report rate-limit bypass — AiSavingsReportService.generateAndSendReport now awaits email dispatch with Promise.all, ensuring the cron job's 500ms breather is honoured and preventing concurrent SendGrid request floods.
Silent 204 on missing tenant context — acceptInsight and rejectInsight endpoints now throw HttpErrors.Unauthorized instead of silently returning, ensuring the frontend is notified when tenant context is missing rather than assuming success.
AI savings report UTC month boundaries — report period start/end dates are now constructed with Date.UTC() to prevent timezone drift from shifting report boundaries when the server's local time is not UTC.
Sentry tag type crash — userId Sentry tag in AiSavingsReportService now uses String(user.id) to prevent numeric IDs from being dropped or throwing SDK warnings.
Inventory setup wizard duplicate-save failure — batch inventory config saves now de-duplicate repeated ingredient rows before persistence, preventing the new setup flow from failing with duplicate-entry 422 errors on staging.
Inventory exports tenant isolation — centralized getTenantHeaders() in API service and correctly applied tenant scoping (x-tenant-id and x-sub-tenant-id) to raw PDF/Excel export requests for Physical Counts and Purchase Orders. This prevents exports from falling back to the incorrect tenant context resulting in 0 items.
Physical Count UI — converted Theoretical, Variance, and Var% columns to use <ng-template>s for instant two-way data binding when the unit dropdown is changed, eliminating thousands of wasted Change Detection cycles. Added Tab key cycle logic for counting inputs to seamlessly focus down the column, and fixed the clickable row cursor using strictly-scoped .table td.clickable CSS.
Inventory navigation tooltip overlay — fixed a bug where the "Inventory is available on the Growth plan" tooltip would appear for accounts that already had access to the feature. Implemented robust clearing by ensuring tooltip text is only assigned when locked and uses empty string fallback in the template.
Purchase Order Security IDOR — added strict validation in updateDraftWithLineItems to ensure line items being modified genuinely belong to the correct purchase order, preventing potential IDOR vulnerabilities.
Purchase Order Calculation Errors — applied correct rounding logic (Math.round) to floating-point multiplication in all purchase order calculations to prevent precision loss.
Gemini CLI configuration — project-level GEMINI.md mirroring CLAUDE.md instructions, 23 project skills, 7 context files, chrome-devtools MCP, and settings for Gemini CLI parity with Claude Code workflows
Inventory dashboard view/edit modal — 3-dots menu now supports viewing and editing ingredients directly from the inventory dashboard via the um-edit-ingredient modal. Uses skeleton loaders and optimistic UI refresh.
Remove from tracking action — 3-dots menu now includes a "Remove from tracking" action with a destructive confirmation warning, reusing the batch update API to untrack ingredients without deleting them.
Purchase Order UI staleness — refactored PurchaseOrderCreateComponent's lineItems array to use an RxJS BehaviorSubject. Ensures UI reacts instantly when items are added, removed, or updated via the catalog dropdown by strictly enforcing new array and object references for the um-paginated-table and assigning ids for accurate tracking.
Inventory dashboard styling — Cleaned up um-paginated-table styling on the inventory dashboard by removing non-standard text-xs classes, fixing vendor and category fallback text, and standardizing the unit cost price change indicator to reduce visual noise.
Goods receipt bulk save — corrected table name (GoodsReceiptLineItem), column name (modified_on = UTC_TIMESTAMP()), and notes null-handling (!== undefined) in bulkUpdateReceived(). Fixes 500 error on draft save and receive-all confirm.
Goods Receiving unit visibility — goods receipt line items now snapshot PO packDescription onto GoodsReceiptLineItem creation so the receiving screen can display the ordered unit context directly from the receipt record instead of relying on a live PO join.
Physical count bulk save — corrected table name (IngredientPhysicalCountItem) and added missing modified_on = UTC_TIMESTAMP() static SET in bulkUpdateCounted(). Fixes 500 error on delta save and count completion.
Bulk SQL tenant isolation — buildBulkCaseUpdate() now enforces AND tenantId = ? unconditionally; tenantId is a required field on BulkCaseUpdateConfig. Eliminates a cross-tenant write risk when callers forgot to include tenantId in extraWhere.
LoginStore cookie/localStorage token merge — when the cookie JWT wins the token selection, all metadata (user, email, name, isNewUser) is now restored from localStorage if the JWT code matches. Prevents tenant resolution from falling back to subdomain lookup and ensures all user properties are preserved. Includes a fix for potential memory mutation when switching tenants and new unit tests.
Changed
Inventory save performance — physical count saveProgress() and goods receiving saveDraft() now use delta-only saves (only send changed items). Reduces 583-item payload to ~4 items in typical use. Optimistic UI via OptimisticUpdateService shows instant success with automatic rollback on error.
Inventory backend bulk SQL — replaced per-row updateById loops with single CASE WHEN SQL statements via shared buildBulkCaseUpdate() utility. Physical count, goods receipt, and inventory config batch operations all use bulk SQL with ELSE fallback, tenant isolation, and chunking at 500 items.
Inventory config batch save — replaced sequential for...of loop (200+ queries) with MySQL INSERT ON DUPLICATE KEY UPDATE (1 query) for the setup wizard.
Inventory dashboard loading — restructured from 6-7 serial waterfall API calls to parallel forkJoin. Critical forks (ingredients, stock) propagate errors; non-critical forks (PO costs, recommendations) degrade gracefully. buildEnrichmentMap() called once instead of twice.
Inventory SSE wiring — added 8 inventory models to MODEL_TO_STORES for cross-user cache invalidation. Fixed handleEvent() guard to support models with empty store arrays (HTTP cache clearing only).
Added
buildBulkCaseUpdate() utility — shared backend utility for parameterized MySQL CASE WHEN bulk updates with ELSE fallback, staticSetParams ordering, chunking, and tenant isolation
floatEquals() utility — safe floating-point comparison for inventory delta tracking, distinguishing null (uncounted) from 0 (zero stock)
Changed
PDF/Excel export redesign — all export templates (PO, physical count sheet, variance report) rewritten to match Demi's Untitled UI design system. Inter font, correct color tokens ($gray-*, $demi-600, $error-600, $success-600), rounded 8px cards, #F5F5F5 table headers, status badges, sentence-case labels. Excel headers updated to brand teal (#089374).
Export filter label — replaced sort label with category filter label in exports. Only shows "Categories: X, Y" when active filters are applied; hidden otherwise.
Added
Dashboard stock inventory export — full PDF and Excel export for the tracked ingredient inventory list with tenant branding, status badges, unit costs, case equivalents, and summary metrics
InventoryExportService + unit tests — backend service for assembling export data with 12 tests covering status classification, filtering, and Excel generation
Fixed
Goods receiving PATCH 400 — text input editQty sent as string; added Number() coercion
Physical count input focus loss — replaced array spread with cdr.detectChanges() to preserve DOM
Excel line items empty — added bypassCache: true to export data loading in PO and physical count services
Tenant name wrong on exports — now shows "Tenant — SubTenant" format
Status badge lowercase in exports — status values title-cased before passing to templates
Typesense 404 on Status sort — added sort: true to inventoryStatus schema field
Wizard stale tracked state — explicit cache invalidation after tx.commit() in batch update
Stock history unit pluralization — wrapped unit names with unitLabel()
Physical count summary bar — redesigned to KPI card grid matching dashboard pattern
3-dot menu label — split "View count" / "Continue count" by status
Multi-category export filter — comma-split for multi-select support
Excel status comparison — use templateType instead of formatted status string
Trial conversion: topbar badge shifts to red at ≤3 days, feature loss preview on billing page, professional success modal after activation
Upgrade confirmation with feature comparison ("What you'll unlock") and prorated charge info
Downgrade warning with feature/data loss impact disclosure and usage vs limits comparison
Annual→monthly interval change warns about lost savings percentage
BillingEmailService — extracted from billing controller (net line reduction) with 6 new email methods: plan upgraded, plan downgraded, trial converted, subscription cancelled, payment retry attempt 2 & 3
6 new SendGrid billing templates — cloned from existing Demi billing email design (account reactivated template)
Unit tests for inactive subscription reason detection — 9 tests covering trial expired, payment failed, voluntarily cancelled, role gating, and edge cases
Feature tier gating (Starter vs Growth) — subscription-based feature access control with HubSpot-style upgrade UX
Shared um-upgrade-lock component — reusable lock overlay with upgrade CTA, replaces 3 copy-pasted NFP/Label Data overlays
Sidebar locked nav items — gated features visible but greyed with arrow icon and upgrade tooltip, click redirects to pricing
AI Insights trigger gating — button greyed with lock icon for Starter, keyboard shortcut also guarded
Backend API guards — 403 on all inventory (37 endpoints) and AI insights (2 endpoints) for Starter accounts
canAccessInventory() / canAccessAiInsights() — new subscription feature checks (FE + BE) with Stripe metadata support
Paginated table cell template support — um-paginated-table now accepts optional cellTemplate (TemplateRef) per column for rendering interactive content (inputs, dropdowns) inside table cells. Additive — existing usages are unaffected.
Reorder Recommendations UX overhaul — collapsible per-vendor sections with pagination at 10 items, "Create PO" button per vendor (creates draft and navigates to edit mode), "Create All POs" button (creates drafts for all vendors, navigates to PO list)
Full inventory table migration — all 14 raw <table> elements migrated to um-paginated-table across 9 inventory components: reorder recommendations, PO detail, PO create, goods receiving, physical count detail, wizard steps 2-4. Editable tables use the new cellTemplate feature for inline inputs and dropdowns.
Migration bastion setup script (setup-bastion.sh) — creates a temporary GCE VM in the production VPC for running the Calgary ETL with sub-10-minute performance (vs 12+ hours over public internet). Supports --env staging|production and --teardown. Credentials fetched on-bastion via Secret Manager (never transit through local machine).
Changed
Pricing page CTA copy — "Start trial" → "Buy [plan name]", removed "No credit card required" (CC is always required for paid subscriptions)
Observable cleanup — added takeUntil(destroy$) on all subscription chains in billing and pricing components, extracted refreshBillingData() to avoid direct ngOnInit() calls
PDF/Excel export redesign — all export templates (PO, physical count sheet, variance report) rewritten to match Demi's Untitled UI design system. Inter font, correct color tokens ($gray-*, $demi-600, $error-600, $success-600), rounded 8px cards, #F5F5F5 table headers, status badges, sentence-case labels. Excel headers updated to brand teal (#089374).
Export filter label — replaced sort label with category filter label in exports. Only shows "Categories: X, Y" when active filters are applied; hidden otherwise.
Inventory module now visible in production — showInventoryModule flipped to true, gated by subscription tier instead of environment flag
Order admin emails now respect notification preferences — wired through sendEmailOnly() with role-based recipients from the notification category registry instead of hardcoded owner+sales list
Order admin email recipients expanded — now sent to Owner, Admin, Manager, and Sales roles (previously only Owner + Sales) matching what the notification settings UI shows
Orders inbox scoped to UpMeals/Calgary tenants only — sendToOrdersInbox() now checks tenant name allowlist instead of sending for all tenants
Billing emails are email-only — switched from sendToUsers() (which created in-app bell notifications) to sendEmailOnly()
"Create All POs" hidden — bulk PO creation button commented out for V1 to reduce UI clutter; individual "Start PO" per vendor is the primary flow
Stock table vendor column — restored with col-width-8 text-xs sizing for compact display alongside 6 other columns
PO Export: PDF & Excel download — single PO export with PDF preview modal, direct PDF/Excel download, and browser print from the PO detail page
PO Export: Batch export — "Export All" from PO list page generates ZIP of filtered POs via Cloud Tasks + GCS, with exponential-backoff polling and progress indicator
PO Export: Duplicate PO — one-click clone of any PO as a new draft for standing/weekly orders
PO Export: Enterprise fields — vendor account number, payment terms, delivery instructions, ship-to/bill-to addresses auto-filled from vendor and tenant/sub-tenant records during PO creation
PO Export: PDF microservice — standalone Cloud Run service (services/demi-pdf-service/) using Puppeteer + Handlebars for server-side PDF generation with SSRF protection and print-safe CSS
PO Export: Auto-fill enrichment — line items automatically populated with vendor product codes and pack descriptions from procurement records (bulk fetch, no N+1)
BatchJob model — new entity for tracking async batch export jobs with status polling, metadata, and GCS file URLs
Migration script (migrate-po-enterprise-fields.ts) — idempotent ALTER TABLE for 14 new columns across Vendor, PurchaseOrder, and PurchaseOrderLineItem tables
Cutover runbook: Datastream CDC fix — replaced unsafe "pause/resume" pattern with "export config → delete → recreate" to avoid primary key conflicts after fresh mysqldump. Added inline export guard (script aborts if config backup missing).
Cutover runbook: timing — updated estimated window from 2h to 4h based on staging dry-run findings
Cutover runbook: pre-cutover steps — added P11 (Datastream config export), P12 (AWS SG whitelist for Cloud NAT IP), P13 (verify NAT IP is static), P14 (bastion VM setup)
Calgary MongoDB migration script — Phase 4 cutover tool that migrates MongoDB collections (users.defaultTenant, user_tenants, QBO tokens, GlobalSettings) after the MySQL ETL completes. Handles ObjectId vs string type mismatch, remaps customerIds/customerLocationIds using the ETL's ID map, forces session resets, and deactivates the old Calgary tenant. Fully idempotent with guard conditions on all writes.
Emergency data export script — Rollback insurance tool that exports records created after go-live timestamp (Orders, Invoices, Transactions, Customers). Exits non-zero on export failures.
GCP cutover runbook — Full operational runbook with verified connection details, pre-cutover checklist, phase-by-phase sequence, rollback procedures, and risk register
IdMap.isNewId() — O(1) reverse lookup to check if an ID is a known new (remapped) value, with lazy-built reverse Set and cache invalidation on set()
UPC conflict warnings now show location name — when a UPC is shared across locations (sub-tenants), the warning dialog displays "Recipe Name (Location)" instead of just "Recipe Name", giving users clear context about which location already uses the UPC
Dashboard showing $0 values when navigating away and back — SubTenantsStoreService.getAvailableSubTenants() was spuriously re-emitting currentSubTenant$ on every cache refresh, triggering the dashboard's tenant-change handler which cleared the sales dataset and cancelled in-progress data loads
Remove stale debug console.log statements from dashboard component and sales dataset service
Inventory wizard save 500 error — batchUpdate procurement query used order: ['createdAt DESC'] but column is created_on; reverted to isDefault: true filter
Pre-existing createdAt column bug in getDefaultForIngredient and ensureValidDefault — both used createdAt instead of created_on
Inventory dashboard and stock history missing unit pluralization — "3 Case" now correctly shows "3 Cases" with smart fallback for units without a plural field (Case→Cases, Bag→Bags, Each stays Each)
Wizard review step showed shelf unit instead of selected unit — user entering "3 Cases" saw "3 Gram" on review; now shows "Case" with converted equivalent ("= 330 Gram")
Wizard PAR step didn't carry over unit selection from stock step — now syncs automatically, with explicit-change flag to prevent overwriting deliberate user choice
Wizard reviewConvertedStock showed raw floating-point numbers — now uses roundForDisplay() for clean output
Cloud SQL upgraded to Enterprise Plus (8 vCPU, 64GB RAM, Data Cache enabled) on staging and production
Connection pool sizes reduced for enterprise scale (main: 40→15, tenant: 5→2) to support 43+ sub-tenants
Cloud CDN enabled on both frontend and backend load balancer services with Brotli compression
Cloud Armor WAF added with login endpoint rate-limiting (20 req/min per IP)
Memorystore Redis upgraded to 8GB (Standard HA)
Startup cleanup job now skips if run within 24 hours (Redis guard with TTL)
Temporarily hidden inventory feature from navigation and route access while feature is finalized
Inventory module loading states upgraded from generic spinners to skeleton loaders that preview the actual page layout (all 8 pages + setup wizard)
Inventory dashboard and PO list filter dropdowns replaced with um-filter-dropdown component matching recipes/orders pattern
Inventory dashboard KPI cards now distribute evenly across all screen sizes (auto-width grid)
Setup wizard empty state redesigned with feature cards (Stock Tracking, Auto Depletion, Variance Analysis) inline on the dashboard — "Get Started" skips the intro and opens the wizard directly at ingredient selection
Setup wizard processing step uses animated progress bar and success step uses animated checkmark with dynamic modal resize — matching production setup wizard patterns
Setup wizard number inputs use text+decimal inputmode with character filtering — prevents letter input and arrow-key increment
All inventory module button labels updated to sentence case
Setup wizard step indicator upgraded from 6px dots to 20px dots for visibility
Physical count list table action links replaced with 3-dots horizontal menu matching orders/users pattern
Table action links now use text-primary teal color for visibility
AI service hosts now configurable via AI_SERVICE_URL env var (removes hardcoded AWS ELB hostnames)
PDF service URL configurable via PDF_SERVICE_URL env var (with hardcoded fallback for backward compatibility)
GCP auth headers gracefully return empty on non-GCP environments (prevents AWS production breakage)
Config validator MySQL host allowlist supports both AWS RDS and GCP Cloud SQL endpoints
Typesense connection uses Connection: close header to prevent ECONNRESET on Cloud Run
Boot sequence: data cleanup now runs AFTER port bind (was blocking PM2 listen_timeout, causing cascading instance replacements)
25-second route timeout failsafe now shows reconnection overlay instead of navigating to /error/timeout (preserves form data)
ER_DUP_ENTRY and ER_DUP_KEYNAME errors treated as non-fatal during schema migration (both default and tenant datasources)
AI insight cost deltas now show accurate per-portion savings (scaled by actual recipe usage quantity)
Cost swap insights are downgraded to flavor upgrade when target costs more (defense-in-depth)
Cost swap insights are downgraded when cost data is missing (prevents $0 hallucination)
Cost delta and percentage are now included on all insight types when both costs are known (enables secondary cost badges)
Typesense sub-recipe search iterates most-specific first (sub-tenant → tenant → master) so local overrides win
Swap animation service applies data mutation even when DOM element is not found (defensive fallback for readonly→edit transitions)
Reports landing page redesigned from a bare dropdown selector into a card-based navigation hub with categorized cards (Sales & Margins, Operations), descriptions, and unique icons for each report type
Goods receipt, production depletion, physical count, and purchase order services all convert to/from inventory units instead of usage units
Default procurement change now automatically syncs the inventory unit on tracked ingredients, preventing unit drift
Dashboard and wizard display the resolved inventory unit (shelf unit from default procurement, falling back to usage unit)
Recipe importer results screen redesigned with clear hierarchy: parent recipe count is the primary metric, sub-recipes and ingredients shown as supporting detail
Removed diagnostic console.log statements and hardcoded company name from recipe import pipeline
Fixed
Subscription leak in SubscriptionService — recipe count fetch now cancels prior in-flight request on token change
Merge conflict in inventory wizard — resolved empty state differentiation for excluded vs missing ingredients
Notification preferences system — org-wide defaults (GlobalSettings) with per-user overrides (User.preferences). Fail-open design: defaults to sending when preferences unavailable
User notification preferences page — individual users can override org defaults for notifications applicable to their role
sendEmailOnly() method — email-only notification path with preference filtering but no in-app bell icon records. Used for billing and order admin notifications
Billing email notifications — invoice receipt, payment failed, and upcoming invoice emails triggered by Stripe webhooks with real SendGrid templates
Customer notification emails — new customer created, customer user added/activated, customer location added — sent to the admin who performed the action
Physical Count PDF/Excel Export — full export pipeline for physical counts. In-progress counts export as count sheets with blank cells for manual entry; completed counts export as variance reports with color-coded variances. Includes PDF preview modal, direct download, and browser print. Backend export service with ExcelJS generation and PDF microservice route.
Physical Count UI Enhancements — category column, per-item notes field (new DB column), count-level notes display, live summary bar (Total Items, Counted, With Variance, Avg Variance %), category filter dropdown, and sortable column headers
Physical Count Cancel — cancel action now wired to backend API in both list and detail views (was a TODO stub)
Dirty Guards — physical count detail and goods receiving pages now prompt Save/Discard on navigation away with unsaved changes (UnsavedChangesGuard + @HostListener('window:beforeunload'))
Reactive Computed Fields — all inventory table value functions converted from pre-computed display strings to reactive inline functions: dashboard reorder table (On Hand, PAR, Suggested Qty, Est. Cost), wizard Steps 2-4 (stock/PAR display labels), physical count detail (variance columns)
Wizard Active Filter — setup wizard now only shows active status ingredients (was showing draft/pending/inactive)
Billing webhook tenant scoping — added tenantId to NotificationOptions so Stripe webhook handlers can pass explicit tenant context for preference filtering and org defaults lookup
Per-user email personalization — sendEmails() now merges user_first_name from each User object into template data, ensuring each recipient sees their own name
Org defaults tenant isolation in webhooks — getOrgDefaults(tenantId) bypasses implicit repository scoping when explicit tenant ID provided, preventing cross-tenant preference reads
SendGrid msg mutation race condition — email msg object now constructed per-recipient inside the loop instead of shared/mutated across iterations
SendGrid silent error handling — added Sentry.captureException to both sendEmail and sendEmailWithTemplate catch blocks
Notification controller missing returns — all 7 notification mutation endpoints now return their promises so errors propagate to HTTP responses
userId safety in notification controller — changed this.user?.id to this.user!.id! on markAsRead/markAsUnread/markAsSeen (auth guard guarantees user exists)
sendByRole null tenant guard — gracefully returns instead of crashing when tenant context is undefined
**markAs* method signatures** — parameters changed from optional to required to match controller assertions
Customer notification DRY — extracted sendCustomerCreatedNotification() private method, eliminating duplicate notification blocks in create() and createWithItems()
Physical Count Variance Reactivity — changing counted amount or unit dropdown now triggers immediate Variance and Variance % re-render via array spread pattern
Cancelled Count Display — cancelled physical counts now show grey "Cancelled" badge (was incorrectly showing green "Completed") and hide edit controls in detail view
Dashboard Reorder Stale Values — removed pre-computed onHandDisplay/parDisplay/suggestedQtyDisplay/costDisplay strings that could go stale; replaced with reactive value functions
Wizard Step Review Staleness — Step 4 review table now uses reactive value functions instead of labels computed at step transitions, preventing stale data when navigating back/forward
Excel Formula Injection — header rows in physical count Excel export now sanitize user-controlled values (tenant name, performer name) through safeText() to prevent formula injection
Blob URL Memory Leak — print popup in physical count list handles blocked popups gracefully and revokes blob URLs via 60s fallback timer
PO Edit mode — draft POs can now be edited from the PO detail page ("Edit" button) or PO list (3-dots menu). Uses ?editId= query param for refresh-safe URLs. In-place update preserves PO numbers via line item CRUD diff.
PO pre-fill from recommendations — "Start PO" on reorder recommendations navigates to PO create form pre-filled with vendor, line items, vendor codes, and unit labels from the recommendation API
PO List actions — 3-dots menu now shows View, Edit (draft only), and Delete (draft only with confirmation dialog). Row click navigates to detail page.
Physical Count actions — 3-dots menu shows "Continue count" (in-progress) and "View results" (completed). Row click navigates to detail.
Below PAR KPI interaction — clicking the Below PAR card smooth-scrolls to stock table filtered by low/critical status (toggles on/off)
Wizard 3-scenario flow — "Add ingredients" wizard now handles: (1) add-only → Steps 2-4 for new items only, (2) add+remove → warning dialog then steps for new items, (3) remove-only → warning then toast and close. Success messages differentiate new vs removed counts.
Backend recommendation enrichment — getReorderRecommendations() now returns vendorCode and purchaseUnitLabel per item, includes procurement unit relations, and excludes ingredients already in open (draft/submitted) POs
Paginated table row click broken — cellAction() only emitted doAction but never called customAction. Fixed to invoke customAction directly with doAction.emit() as fallback. Also added isActionHidden/isActionDisabled guards on default action for isCellClickable.
Line item status label — draft PO line items now show "Draft" badge instead of "Pending"
Column spacing across all inventory tables — added proper cssClassHeading with col-width-* classes to 10 tables (stock-history, PO list, PO create, PO detail, physical count list/detail, goods receiving, wizard steps, reorder recommendations, stock levels)
Responsive table headers — added text-nowrap to all inventory table header classes to prevent overlap on narrow viewports
Sidebar navigation — parent categories with children (Sales, Recipes, Inventory, etc.) now only expand/collapse on click, no longer navigate to first child
Wizard double untrack warning — removalConfirmed flag prevents showing removal warning twice in add+remove scenario; flag resets when navigating back to Step 1
Dashboard "None" → "-" — Last Count KPI shows dash instead of "None" when no counts exist
Dashboard KPI font size — bumped from display-xs to display-sm for better visual balance
Cost compact formatter — Est. Reorder Cost KPI now shows $97.5K for large values, drops decimals above $10K
"Save & submit" → "Save & create" — button text updated since submit workflow isn't implemented yet
Expected Delivery Date mandatory — now required field on PO create form with * label and canSave validation
Unit Cost read-only on PO create — displayed as formatted text, not editable input (managed at procurement level)
LOCAL-DEV-GUIDE password fix — corrected $set escaping in docker exec node script (single quotes instead of triple-backslash double quotes)
PO Create: vendor dropdown empty — loadVendors() called fetchItems() without subscribing to the returned Observable, so the API call never fired. Vendors now load correctly with error handling
Missing FontAwesome history icon — added faClockRotateLeft to icon registry for the "View history" stock table action
Nutrition data display: unsubscribed fetchItems() — same cold Observable pattern bug as PO Create, now properly subscribed with takeUntil(destroy$) lifecycle cleanup
Inventory pluralization bugs — unit names now correctly pluralize in reorder recommendations ("3 Cases" not "3 Case"), stock level fallback display ("0 Grams" not "0 Gram"), and PAR level fallback display
"No vendor" badge styling — added missing badge-pill class for visual consistency with status badges (Ok, Low, No PAR)
Redundant zero sub-text — stock and PAR columns no longer show "0 Grams" secondary text when the primary amount is already zero
Wizard SCSS hardcoded colors — replaced #05B09B and #e1e9ee with $teal and $gray-200 SCSS variables
HTML entity escaping — escapeHtml helpers now escape single quotes (' → ') for complete attribute-context safety
XSS prevention — all user-derived strings (ingredient names, unit names, pack sizes) are now escaped via escapeHtml() before rendering in html: true table fields
Invisible wizard error — pre-validation conversion errors now show a visible alert dialog instead of silently setting an unreachable error state
Invalid CSS class — replaced non-existent color-error-500 with text-danger
Dead animation code — removed unused slideDown animation trigger and @angular/animations import from wizard
Backend Function types — replaced untyped Function generics with specific method signatures in ingredient-procurement.repository.ts
PO create arrow symbols — replaced HTML entities (↑/↓) with unicode characters in Angular interpolation
Emergency export column names — fixed createdAt → created_on to match actual MySQL schema. Script was silently skipping all 11 tables. Added OrderTransaction export via Order JOIN (table has no timestamp column). Fixed env var fallback from MYSQL_ROOT_PASSWORD → TARGET_SQL_PASSWORD.
Reorder suggested quantity inflated 12x for 3-tier unit ingredients (Case→Each→Gram) — now uses shelfToCase() utility for correct conversion in both createFromBelowPar and getReorderRecommendations
Physical count save/complete 422 error — countedAmount was sent as string from text input; added Number() coercion in toShelfAmount()
Reorder recommendations table showing raw shelf unit values (e.g., "2640" instead of "2 Cases") on page reload — fixed race condition by chaining loadRecommendations() after ingredientMap is populated
PO list showing persistent skeleton loaders when empty — merged dual template blocks into single um-paginated-table with proper loading/empty state
Inventory wizard closing on Escape key without confirmation — added keyboard: false to modal config
Physical count unit column displaying "×" prefix from ng-select clear button — added CSS override
BigQuery queries use SQL aggregation (SUM, AVG, ARRAY_AGG) instead of pulling large datasets into Node.js memory
SSO tenant ID cross-validated against Firebase token's cryptographic tenant claim (prevents cross-tenant replay)
BigQuery project ID from GCP_PROJECT_ID env var (not hardcoded to staging)
New environment variables (FIREBASE_*, GCP_PROJECT_ID) documented in .env.example
SSE memory leak from write buffer accumulation — response.write() return value was ignored, causing libuv write buffers to grow unbounded (~16 MB/connection/hour) when clients can't consume data fast enough; added backpressure detection (draining flag), drain event recovery, and 64KB zombie connection kill switch on heartbeat
SSE memory leak — empty catch blocks in heartbeat and event handlers silently swallowed ERR_STREAM_DESTROYED errors, causing orphaned timers and listeners to accumulate permanently; added idempotent cleanup on write error or disconnect
CORS origin reflection on SSE endpoint — previously reflected any Origin header verbatim with credentials: true; now validates against the same allowlist used by the main CORS middleware
EventBus listener dispatch concurrent mutation — dispatchToLocalListeners now snapshots handler Sets before iterating, preventing handlers from being skipped when a sibling unsubscribes mid-dispatch
Renaming a product not updating in product lineups — product changes now invalidate the product lineups cache so updated names appear immediately
Nested relation data (e.g., product names in lineups) could appear blank when served from Redis cache — added inclusion-aware cache bypass to 15 repository relation resolvers
Health endpoint fragility — sequential tenant DB checks + no aggregate timeout caused all 3 instances to be replaced during a Redis blip; added 10s aggregate timeout, parallel tenant checks, smart partial responses, and structured diagnostic logging
clean-progress-tasks cron killing leader instance — full table scan loading 135 MB of longtext blobs into memory every hour; replaced with SQL-filtered queries and field projections
Silent process.exit in PM2 — unhandledRejection, uncaughtException, and graceful shutdown handlers now use synchronous process.stderr.write instead of console.error
Calgary ETL: dashboard revenue showing $0 and food cost 0.0% after migration — Order, Invoice, and ShoppingCart items stored old Calgary Product IDs inside JSON blob columns; new post-migration phase remaps embedded IDs using the IdMap built during migration
Calgary ETL: added multi-tenant safety guard — JSON remapping skips tables without subTenantId column to prevent unscoped operations across all tenants
Implausibly high cost deltas ($20+/portion) in ingredient swap insights — cost deltas are now normalized by recipe yield and capped at $3/portion
Sub-recipe cross-utilization count missing in AI payload when Typesense returns no candidates — used_in_count now initialized to 0 on all current sub-recipes
Cron jobs silently disabled on AWS after deploy — INSTANCE_ID not set in PM2 process because Node.js IMDS fetch timed out during heavy post-deploy I/O; shell scripts now fetch INSTANCE_ID via curl before pm2 start/reload, providing a reliable pre-set value
Silent cron failure logging — claimLeadership() now logs an error when IMDS was attempted but INSTANCE_ID is absent, instead of silently returning
AI insights trust boundary — runtime type guards for AI microservice response fields (confidence, title, description); analyzedAt falls back to current timestamp if missing
Inventory purchase order detail buttons using non-existent CSS classes — now use Demi design system classes (btn-destructive, btn-primary)
Inventory purchase order detail cancel action had no confirmation dialog — now requires destructive confirmation
Inventory setup wizard error state layout broken with misaligned text and no escape route — now centered with both Close and Try Again buttons
Inconsistent top spacing across inventory pages — normalized with page-header wrapper
"Ingredient #123" developer debug text shown to users when ingredient name is unavailable — replaced with "Unknown ingredient" across all inventory pages
Missing back-navigation on stock history and physical count detail pages — added "Back to..." links matching other detail pages
Goods receiving page missing "Expected Qty" column — users can now see ordered quantity alongside received quantity
Setup wizard category dropdown hidden when no categories exist in dataset
Recipe costing returning unit-mismatch error when mixing weight and volume units (e.g., g + ml) — added weight↔volume conversion using 1ml≈1g density fallback; extracted shared resolveAmountMultiplier() helper to eliminate duplicate decision trees
Unit mismatch warnings not showing specific unit details — propagated backend message field (e.g., "usage unit 'g' vs yield unit 'ml'") to frontend warning display
Direct SQL inserts with deleted = NULL causing 404 errors — added pre-migration script to batch-update NULLs and enforced NOT NULL DEFAULT 0 on the deleted column
Cloud Run cron self-calls use dynamic PORT (Cloud Run assigns port at runtime, not always 3000)
Seasonal swap recommendations powered by USDA AMS market data — replaces standalone "in season" alerts with actionable swap suggestions (e.g., "Swap Cauliflower → Broccoli, 26% below market average")
USDA AMS Market Service matches inventory ingredients against real-time commodity price data to identify in-season items
Hallucination guard drops seasonal swap suggestions when AMS data is unavailable or target ingredient isn't verified as in-season
Seasonal impact labels show cost savings when available (e.g., "$0.40/Portion Saved · In Season")
HTTP retry interceptor for GET requests — retries 502/503/504 with exponential backoff (100ms→200ms→400ms), transparent to users
Reconnection overlay — shows "Reconnecting..." spinner when backend is unreachable, auto-dismisses when /health returns 200
ProductionDaysRecord duplicate cleanup added to post-start data cleanup (prevents migration ER_DUP_ENTRY on next boot)
Sub-recipe swapping paradigm for AI insights — swap entire sub-recipes (e.g., sauces, glazes) for allergen-free or lower-cost alternatives
Allergen removal insight type with red badge, health & safety icon, and "Allergen-Free Alternative" impact label
Sub-recipe library fetched via Typesense (same-category search) with DB enrichment for allergen data
Cost data for sub-recipe candidates via Phase 3 fast-forward (candidates included in batch cost calculation)
Slot reservation: up to 2 of 3 insight slots reserved for sub-recipe swaps when context is present, with graceful fallback
Cross-utilization counts (used_in_count) included in sub-recipe AI context — enables the AI to prefer widely-reused sub-recipes
USDA AMS price history accumulation — daily price observations stored in usda_ams_price_history for true seasonal signal computation
90-day rolling average and commodity-specific season windows derived from historical AMS data (replaces synthetic current+next month windows)
Bootstrap endpoint (GET /cron/usda-ams-history-bootstrap?days=180) for backfilling historical price data via ProgressTask
Seasonal savings now reflect true seasonal signal (seasonalSavingsPct from 90-day rolling average) with fallback to inter-market price spread
Logging middleware memory leak — response event listeners now use .once() and are explicitly removed on error paths, preventing closure accumulation under sustained 422 traffic
Production boot timeout: data-cleanup (157s+) no longer blocks port bind, preventing PM2 from killing the process at 120s listen_timeout
isShuttingDown flag now resets after cleanup, allowing re-initialization in test environments
/health endpoint excluded from HTTP cache interceptor (prevents stale cached health responses masking outages)
AI insights now apply correctly when accepted in readonly mode (added event loop yield for bs-sortable DOM rendering)
Undo correctly cancels all in-flight animations (array of handles replaces single handle, eliminating race condition)
Applied impact label now correctly transforms "$X/Portion Saved" format (was checking wrong prefix)
Batch apply bar no longer shows "$0.00" when non-cost insight types dominate
CodeDeploy no longer overwrites nginx reverse proxy config — yum update removed from pre-install hook, nginx.conf is now restored on every deploy
Deployment scripts (stop.sh, start.sh, validate.sh) upgraded from unsafe 1-liners to full safe lifecycle with ALB deregistration/re-registration, environment auto-detection, config validation, and memory limits
Validation timeout increased from 90s to 300s to accommodate LoopBack 4 cold starts
Nginx port 80 health check added to deployment validation — catches broken reverse proxy before ALB routes traffic
Multi-date range KPL reports now work correctly across all timezones — removed all timezone double-conversions in the date picker round-trip (both outgoing in PR #996 and incoming in PR #1002), so dates no longer shift backward on each render cycle in non-PST browsers
Bulk status changes now use correct lowercase EntityStatus enum values matching the status badge renderer
Bulk category changes include denormalized Typesense field names so category names display immediately
SweetAlert2 preConfirm leak between dialogs (e.g., category picker validation appearing in delete modal)
Recipe soft-delete no longer fails with "Company UPC prefix is not configured" for tenants without UPC setup
Ingredients bulk operations no longer fire N redundant Typesense reload requests
Edit modal now shows fresh data after bulk status/category changes instead of stale cached values
Inventory tables now created on staging/production — schema migration runs before tenant datasources are initialized, preventing tenant DB failures (e.g. duplicate-key conflicts) from aborting table creation on the shared database
Material Symbols font added to staging index.html so inventory wizard icons render correctly
Schema migration resilient to duplicate key errors with critical table creation fallback and VARCHAR(255) key length compliance
Six missing await calls on delete operations in the audit repository — operations now complete before audit logs are written
Concurrent goods receipt confirmation no longer throws a misleading 422 — gracefully returns when receipt is already confirmed
Physical count item updates now run in parallel instead of sequentially, improving performance for large count sessions
Purchase order auto-generation now clamps look-ahead days to 0–30, preventing unbounded demand queries
Inventory config batch creation now applies field allowlist, preventing tenant field injection on the create path
Timezone-dependent test failures fixed in DashboardDataService and DatesHelper specs
Sentry error handler no longer crashes on string exceptions — beforeSend now guards the in operator against non-object values, preventing silent bypass of the 4xx error filter
Missing Font Awesome icons (arrow-down, arrow-up, clone) now render correctly across ingredient price history and duplicate action buttons
Kitchen Production List no longer crashes when recipes haven't loaded yet — the recipes dictionary is now initialized and all access points are guarded
TypeSense-powered tables no longer crash when the collection config arrives after initialization — component now handles late-arriving inputs via ngOnChanges
Edit Order no longer crashes with "Cannot read customerLocationId of undefined" — fixed a useless if (!this) guard that should have been if (!this.item)
Orders list sorting no longer crashes when a customer array entry is null — added optional chaining to the sort comparator
Management list pages no longer crash when URL contains duplicate sortby query parameters — gracefully extracts first value from array params
Audit log infinite scroll no longer crashes when the load-more element is unavailable after component teardown
Recipe import controller no longer registers duplicate legacy endpoints that conflicted with ChefTecImporterController
Recipe import URL validation now prevents SSRF by requiring S3 URLs and uses proper HTTP error codes instead of generic errors
Ingredient cache key normalization now matches database lookup normalization, preventing duplicate ingredient creation during import
Recipe importer no longer leaks subscriptions — all observable chains are properly tracked and cleaned up on destroy
Recipe import progress callbacks no longer produce NaN when total is zero
Recipe importer components properly declared in NgModule instead of using standalone mode
scrape-expired-tokens: added missing leader guard (shouldRunServerTasks) with fail-open for idempotent operation
Disabled 2 dead cron jobs (process-sv-expired-products, signifi-batch-products-sync) that were running hourly with no-op controllers
Removed
All raw <table> elements from the inventory module — replaced with um-paginated-table
Manual skeleton loading tables — replaced by um-paginated-table's built-in [loading] state
Dead viewPurchaseOrders() method and getLineStatusLabel() method
Performance
Cron Phase 4: configurable dispatch delay — non-Stripe crons (clean-progress-tasks, create-production-days, b2b-reminder) now use 1s delay between tenants instead of 3s, cutting sleep overhead by 67%
Cron Phase 4: clean-progress-tasks converted to Pattern B — eliminated 162 HTTP dispatches/hour by running one cross-tenant cleanup query directly (ProgressTask has no tenant scoping)
Cron Phase 4: place-meal-plan-orders narrowed to business hours — schedule changed from hourly 24/7 to weekdays 8 AM–6 PM, eliminating 13 no-op runs per day
Cron Phase 2: founder-followup-email — reduced from every 30 min to daily at 7 AM (was causing ~100% CPU duty cycle on leader with 29 min avg runtime)
Cron Phase 2: kpl-cache-prewarm — replaced N+1 tenant→subtenant query loop with single batch query filtered for non-empty productionDays; 2 DB queries regardless of tenant count
Cron Phase 2: b2b-reminder — new shouldRunAtThisTime() hook skips entire HTTP tenant dispatch loop when no B2B-reminder-enabled customers exist; checked after leader election to avoid redundant queries on non-leader instances
Cron Phase 2: kpl-cache-prewarm — added DISABLE_CRON_KPL_CACHE_PREWARM env var support; fixed singleton logger race condition by making logger a local variable
Per-job DISABLE_CRON_* env var mechanism — disable any cron via environment variable without code changes (e.g., DISABLE_CRON_CLEAN_PROGRESS_TASKS=true)
CronJobLogger: duration and heap metrics on every cron end, Sentry captureMessage alert for slow crons exceeding configurable threshold (default 5 min)
GLOBAL_COLLECTIONS constant in typesense-collection-schemas.ts — centralized Set of non-tenant-scoped Typesense collections (usda-fdc-branded, usda-fdc-foundation, usda-fdc-sr-legacy, open-food-facts)
UsdaFdcCronJobBase abstract class and runUsdaFdcPipeline() standalone function — shared download → validate → import pipeline with Redis lock, extracted for testability
14 unit tests covering all pipeline branches including circuit breaker fail-open, lock lifecycle, and cleanup edge cases
CostingService.prewarmRawPacks() — lightweight login-time method that loads costing data packs without computing individual entity costs
GET /cache/status endpoint — lightweight Redis EXISTS check for cache readiness monitoring
DataPackConfig.priority field — packs tagged as 'critical' or 'background' for tiered pre-warming
Category column to inventory dashboard Stock Levels table — reads categoryName directly from Typesense, sortable
Inventory wizard tracked state — pre-checks already-tracked ingredients with "Tracked" badge, loads configs in parallel via forkJoin
Wizard untracking — unchecking a tracked ingredient triggers soft untrack (isTracked: false, data preserved) with two-tier SweetAlert confirmation (standard for ≤50, type "REMOVE" for >50)
Database transaction on inventory config batch endpoint — wraps all create/update operations in READ_COMMITTED transaction for atomicity
Enterprise architecture for Cactus Club (43+ locations) — bulk sub-tenant onboarding API with CSV parser, 3 data modes (inherit/copy/independent), async Cloud Tasks processing for >10 locations
Enterprise HQ dashboard with BigQuery-powered analytics — revenue rollup, food cost analysis, location benchmarks, vendor comparison (all aggregation in SQL, not Node.js)
Enterprise SSO via Google Cloud Identity Platform — Firebase Admin SDK strategy with JIT user provisioning, domain restriction, per-tenant SSO configuration UI
Enterprise offboarding — deactivate/archive/reactivate sub-tenants with Redis cache flush and bulk processing
Location wizard "Inherit from HQ" option — new locations see HQ master recipes via existing findMerged() without data copying
Bulk CSV upload UI for mass location onboarding with drag-and-drop, preview table, per-row validation, and progress tracking
Inventory procurement unit foundation — switches inventory tracking from recipe usage units (grams, cups) to procurement shelf units (lb, kg, l) across wizard, dashboard, physical counts, and stock history
Backend unit conversion utility (unit-conversion.util.ts) with caseToShelf, shelfToCase, isSameClassConversion, formatAmount, and roundForDisplay functions
Frontend unit conversion utility (inventory-unit.util.ts) mirroring backend logic plus formatInventoryAmount and getCountByUnitOptions for display formatting
defaultProcurementId and countByUnitIds fields on IngredientInventoryConfig — links configs to their default procurement and stores allowed count-by units
Setup wizard: ingredients without procurement data are filtered out, Vendor and Pack Size columns replace Category and Unit, unit dropdown on stock and PAR steps with automatic shelf-unit conversion on save
Inventory dashboard: Vendor column replaces Category, dual-line On Hand display showing case count as primary with shelf-unit equivalent as secondary
Physical count detail: unit selector dropdown per item with automatic theoretical/variance recalculation when switching units, amounts converted to shelf unit on save
Stock history: amounts display in shelf unit with case equivalent in parentheses (e.g., "+30 lb (3 cases)")
One-time migration script (migrate-inventory-units.ts) to update existing tracked configs with procurement data, flag cross-class conversions, and remove trackingless ingredients
Procurement change hook: auto-updates inventory config when default procurement changes on a tracked ingredient
Memory telemetry observer — logs heap usage, EventBus listener counts, and active SSE connections as structured JSON every 60s for CloudWatch monitoring
Evidence-based incident response protocol added to CLAUDE.md — AI agents must verify root causes from logs and metrics before proposing fixes
Seasonal insights quality hardening — self-swap guard filters AI-generated insights where source and target are the same ingredient; cost-aware Scenario A/B presentation hides misleading cost increases on seasonal alerts while highlighting confirmed vendor savings; regional personalization adds market context to seasonal rationale
Minimum 10% seasonal savings threshold — filters out marginal AMS price signals (was 0%) to surface only actionable seasonal insights
AMS bootstrap now upserts current prices alongside history records so seasonal signals work immediately after initial data load
Season-aware insight labels — seasonal alerts now show "In Season" when the ingredient is currently in its peak window, or "Peaks Sep–Nov" when off-season, with tense-appropriate rationale text
Dynamic region in seasonal insights — resolves subtenant city/province from address configuration (e.g., "Vancouver, BC market") instead of hardcoded "North American market"
Inventory dashboard "Print / Export" dropdown with Print, PDF, and Excel options (placeholder stubs for export logic)
Physical count list 3-dots action menu with View details, Print, Export PDF, Export Excel, and Cancel count options
Physical count detail search bar for filtering ingredients during counting
Physical count cancel button with destructive confirmation dialog on both list and detail pages
Inventory dashboard "Ingredients" page header with consistent top spacing
Setup wizard "Skip for now" option on stock amounts and PAR levels steps
Per-item vendor resolution in ingredient importer — uploads with a vendor column now assign each item to its correct vendor (with auto-create and per-import caching)
Copy-paste detection for multi-tab XLSX recipe imports — warns when two tabs have identical ingredient lists (with Unicode NFC normalization)
Expanded cache management entity allowlist with 7 additional model types
Internal cron jobs failing with 422 login errors since July 2024 — CallCronJobService resolved password binding at construction time (before observer set it), causing ~19,450 failed POST /auth/login requests/day from 127.0.0.1; now uses lazy @inject.getter() to resolve at call time
Double-delay bug in cron tenant iteration — rate-limited tenants slept 5s inside catch + 8s inter-tenant (13s total); consolidated to single 8s delay
Leadership election race — all 3 EC2 instances fired claimLeadership() at the same cron second; added 0-5s jitter to spread out claims
getLogger wrapper silently discarded Error stack traces — error()/fatal() methods now preserve .name, .message, .stack alongside service metadata
Inconsistent validateStatus in cron endpoint calls — was < 400 (accepting 3xx) then rejecting non-200; now uses status === 200 only
Unstructured logging in leadership election — replaced console.log/console.error with structured logger for Errsole visibility
GCP migration design spec and implementation plan (docs/superpowers/specs/ and docs/superpowers/plans/)
Nginx rate limiting on /auth/login — 2 req/sec per real client IP with burst allowance, returns 429 when exceeded (mitigates credential stuffing attack)
Real IP extraction via nginx realip module — trusts only VPC CIDRs so X-Forwarded-For cannot be spoofed to bypass rate limits
X-Forwarded-For logged in request metadata for attack forensics and IP visibility
Checkbox multi-select with floating bulk action toolbar (HubSpot/Notion-style) for Ingredients, Sub-Recipes, Products, and Orders list views
Bulk change status, change category, and delete actions for Ingredients, Sub-Recipes, and Products
Bulk change status (Upcoming, Cancelled) for Orders with destructive confirmation modal for cancellation
Shift+click range selection and tri-state header checkbox in all list tables
Backend batch endpoints with 250-item hard cap and per-item error reporting
Optimistic UI updates for status and category changes — values appear instantly without waiting for Typesense re-index
Direct Typesense sync from batch controllers to ensure denormalized fields (category names) persist after re-index
Demi AI Insights panel in the recipe editor — premium branded side panel with animated loading (quantum loader), insight cards with category badges, visual impact indicators, expandable "Why this suggestion?" rationale, swap preview popovers on Apply hover, and batch "Apply All" for non-conflicting suggestions
Proactive AI badge on the Insights trigger button — shows a notification dot when cached insights are available, with Cmd+I / Ctrl+I keyboard shortcut to toggle the panel
Savings counter in the panel footer — tracks cumulative savings from applied cost swap insights
Backend enrichment for AI insights — ingredient names and server-generated rationale included in API response, plus a lightweight cache-only /insights/available endpoint
Global AI identity SCSS system (_ai-vars.scss + _ai.scss) with reusable gradient, glow, and animation classes for future AI features
Manual purchase order creation — operators can now create ad-hoc POs from a new full-page form at Inventory → Purchase Orders → Create
Ingredient search filtered by vendor catalog with two-line autocomplete (name + vendor code, unit, price)
PAR stock indicators on PO line items — red/green/gray dots show whether ingredients are below, above, or not tracked against PAR levels
Price change indicators comparing current procurement cost vs last PO for the same vendor
Quick reorder button to pre-fill line items from the vendor's most recent purchase order
Sticky action bar with running subtotal, Save Draft, and Save & Submit actions
Shared um-empty-state component for consistent empty states across the app — supports icons, title, description, and CTA button
Shared um-stock-status-badge component for displaying inventory stock level status (OK, Low, Critical, Out of Stock)
Global SCSS partial _um-empty-state.scss with Demi design tokens for the empty state pattern
UI mockup previews workflow (.ui-previews/) for rapid design iteration before Angular integration
Inventory now tracks stock in procurement shelf units (kg, L, lb) instead of recipe usage units (Gram, Milliliter) — operators see units matching what's on their shelves
Wizard intro step now shows SVG icons for Stock Tracking, Auto Depletion, and Variance Analysis feature cards
Automated onboarding email sent to Sysco when a customer connects their account, including CC/BCC to internal stakeholders
First sync completion email and in-app notification when Sysco data arrives for the first time
Daily in-app notification showing price update count, direction breakdown, and estimated time saved
Sync error email and in-app notification when Sysco sync fails, with automatic retry messaging
Stale pending warning email after 72 hours if a configured Sysco account receives no data
"Sysco" notification tag with green badge in the notification center
Sysco integration widget now shows last sync summary (items matched, price changes) and cumulative time saved this month
Relative time display for last synced (e.g., "just now", "5 hours ago", "yesterday")
Stale data warning in the widget when last sync is over 48 hours old
Success toast when submitting Sysco integration and when transitioning from pending to connected
"What happens next?" expandable section during pending state
CC and BCC support for outbound emails via SendGrid
AI Insights panel in the recipe editor — an on-demand side panel that shows up to 3 actionable ingredient suggestions (cost savings, flavor upgrades, seasonal swaps) with one-click apply, full ingredient data swap, and a 7-second countdown undo toast
Backend endpoint for AI product insights — proxies to the Python AI microservice, enriches responses with cost deltas and sub-recipe metadata, and caches results for 5 minutes
Cron audit: regenerate-product-caches — single updateAll replaces N+1 find+loop invalidation
Cron audit: email crons — push DB-level WHERE filters for trial-reminder, founder-followup, onboarding-call (eliminates full tenant table scans); batch user/userTenant lookups
Cron audit: overlap protection — new withCronLock Redis distributed lock utility applied to b2b-reminder, typesense-sync, regenerate-product-caches, kpl-cache-prewarm
USDA FDC cron jobs (branded, foundation, sr_legacy) now execute globally once instead of once per tenant — eliminates 49 redundant 400k+ record imports per month with 50 tenants
Redis distributed lock on USDA FDC sync prevents concurrent manual + scheduled runs with fail-open on Redis degradation
Cold-cache login optimization — parallel pre-warm of critical data packs (recipes, ingredients, units) + costing raw packs at login time with p-limit(3) concurrency, reducing cold-cache dashboard load from 15-20s to <5s
Thundering herd protection on data pack loading — Redis NX lock prevents 20+ concurrent cold-read API calls from stampeding the database with identical queries
Recipe metadata-for-edit endpoint parallelized — 4 sequential server-side queries (ingredient list, allergens, used-in, sold-as) now run via Promise.allSettled
Login controller gates on critical cache packs via 10s Promise.race timeout before returning response
perf_hooks timing on all data pack loads for observability
Release
4.15.0
Added
Ingredient inventory management module — track on-hand stock levels with PAR thresholds, low-stock alerts, and a setup wizard for initial configuration
Physical count sessions — create count sessions, enter counted amounts, and auto-generate inventory adjustment entries based on variance
Purchase order management — create, edit, submit, and cancel purchase orders with vendor-grouped line items and procurement-based unit costs
Demand-driven PO auto-generation — generates purchase orders based on upcoming production demand (configurable look-ahead period) plus PAR safety stock, replacing the previous PAR-only formula
Goods receiving workflow — receive against submitted POs, enter quantities, confirm receipts with automatic stock entry creation and unit conversion
Reorder recommendations on the inventory dashboard — shows below-PAR and demand-driven reorder suggestions grouped by vendor with estimated costs
Stock history view — chronological log of all stock movements (initial counts, production depletion, physical counts, goods receipts, adjustments)
Physical Counts link added to inventory sidebar navigation
Fixed
Inventory stock history now shows the ingredient name instead of "Ingredient #1217"
Physical count "Completed At" column now displays a formatted date instead of a raw ISO timestamp
Stock history "Current On-Hand" no longer flickers to 0.00 while loading
Inventory setup wizard dashboard refresh is now reliable — uses modal onHidden event instead of firing during modal dismissal
Inventory setup wizard "Select All" now only affects filtered ingredients when a category or search filter is active
Inventory setup wizard ingredient list now uses virtual scrolling for smooth performance with 650+ ingredients
Inventory pages now use consistent page headers, heading sizes, loading spinner spacing, and filter bar styling
Recipe import URL validation now prevents SSRF by requiring S3 URLs and uses proper HTTP error codes
Recipe import progress no longer produces NaN when total is zero
Child entity changes (recipe ingredients, sub-recipe links, tags, etc.) in audit logs now show parent context — e.g., "Product — Beef Bibimbap > Beef, Bulgogi" with clickable links to parent and component entities
Audit log viewer now shows human-readable entity names (e.g., "Beef, Bulgogi" instead of "ID: 3357"), operation badges, user avatars, relative timestamps, and humanized property names — replacing raw IDs and camelCase throughout
Audit log detail view no longer shows system metadata fields (modifiedOn, tenantId, etc.) as changes — these are implied by the log entry itself
Editing or deleting kitchen stations, units, and other category types in Settings > Categories now immediately refreshes related list views instead of showing stale data until the next background sync
Audit log viewer now displays data correctly — fixed a dead subscription caused by accessing a ViewChild before it was resolved, which silently killed all data rendering since Feb 21
Post-deployment zombie instances no longer occur — TypeSense collection recovery now waits 30 seconds after startup and adds 5-second breathers between collections, preventing event loop saturation that blocked health checks
Sysco sync now runs in update-only mode — vendor code matching updates existing procurements instantly, and catalog items not in the tenant's ingredient library are skipped without AI processing
Sysco sync vendor code matching now completes in seconds instead of timing out — batch-fetches all vendor procurements in one query instead of 18K+ individual lookups
Sysco sync now always updates the existing procurement record instead of creating duplicates on different days, and no longer overrides isDefault when another vendor is set as default
Save button now enables immediately when changing a round label's diameter
Optimistic save no longer shows contradictory "saved" and "failed" toasts when a background save fails
Saving orders, customers, recipe categories, recipe types, and ingredient categories now immediately refreshes related views without waiting for background sync
Fixed memory leaks from untracked subscriptions in recipe editor and recipe summary view
Sysco sync no longer times out during processing — vendor code matching now runs first (golden path), and AI validation only runs on the small subset of unresolved items instead of all 18K+ EDI items
ALB health check now detects zombie instances within ~40 seconds instead of ~5 minutes by using the /health endpoint (verifies MySQL, MongoDB, and Redis) instead of static HTML
IMDS instance ID fetch no longer silently fails open — uses IMDSv2 with retry and prevents all instances from running cron jobs simultaneously when metadata is unavailable
Changing recipe category or workflow now immediately enables the save button (dirty guard was not triggering for these fields)
Duplicating a product or sub-recipe now shows "Creating duplicate..." and "Duplicate created" toasts instead of generic save messages
KPL date range picker no longer freezes or floods API requests when browser timezone differs from app timezone (e.g., JST vs PST)
Fixed KPL date range picker producing garbage dates (year ~2000, 1961, or 1861) that froze the browser or corrupted URL state
Fixed date range picker briefly displaying "Dec 31, 1999 – Jan 5, 2000" when switching to date range mode across all report and KPL pages
Header layout no longer compresses when trial banner or subscription badges are visible — search bar scales gracefully at all viewport widths with consistent spacing between elements
Multi-day KPL print and export now has a "Only show days with data" toggle that hides empty date columns, making reports with sparse data easier to read
Audit log UPDATE operations now store full before/after snapshots — enables parent context resolution for child entities and richer diff display
Entity name resolution service for audit logs — fetches human-readable names from entity APIs (recipes, ingredients, products, etc.) with batched requests and in-memory caching
Operation type filter on audit log viewer (Created, Updated, Deleted, Restored)
Backend now captures user's first and last name in audit log entries for proper display names
/liveness endpoint provides a lightweight probe (no database checks) for debugging instance health during startup
Startup timing markers log duration of each initialization phase (boot, migrations, observers) for faster root-cause analysis of slow starts
/health endpoint now checks MongoDB connectivity (db.command({ping:1})) and Redis availability (PING) in addition to MySQL, all running in parallel
Zombie instance detection and recovery runbook added to EC2 health check guide with step-by-step commands
CloudWatch alarm documentation for UnHealthyHostCount on both production and staging ALBs
Safe restart scripts (restart-staging.sh, restart-prod.sh) for manual SSM restarts — validates config and verifies process health after restart
EC2 health check guide now documents safe manual restart procedures and warns against pm2 delete all
Diagnostic API endpoint (GET cron/sysco-sync/diagnostic) for inspecting Sysco procurement data quality — shows duplicates, orphans, match quality breakdown, and sample records
Read-only verification script (scripts/verify-sysco-sync.js) for validating Sysco sync results against the database
Changed
MySQL connection pool increased from 20 to 40 connections to prevent pool exhaustion during startup when observers, cron jobs, and health checks compete for connections
Backend deployments now validate ecosystem config for placeholder values and correct MongoDB host before starting PM2, preventing crash-loops from misconfigured instances
CodeDeploy stop scripts use graceful PM2 shutdown (pm2 stop + pm2 kill) instead of raw SIGKILL, preventing corrupted PM2 state on restart
Deployment health check timeout increased from 90 seconds to 150 seconds, reducing false validation failures during cold starts
ALB re-registration in deployment validation now fails the deployment if IMDS metadata is unavailable, preventing instances from going dark after a "successful" deploy
Sysco EDI 832 sync now produces a structured summary report with match statistics, price changes, and error counts instead of verbose per-item logging
Sysco sync now caches unit lookups per run, eliminating thousands of redundant database queries per file
Sysco sync idempotency improved with 3-tier procurement lookup fallback (vendor code, units, Sysco source), preventing duplicate procurements on re-runs
Release
4.14.1
Fixed
Duplicate recipe and product names can no longer be created through the UI — validation now runs on all create and update endpoints with fresh database queries, preventing stale Redis cache from allowing duplicates
Renaming a recipe or product to an existing name in the same tenant now returns a clear error message instead of silently succeeding
UPC codes are no longer duplicated across recipes — fixed 4 root causes: Redis lock silently succeeding on failure, regeneration bypassing distributed lock, cross-tenant scans missing the default datasource, and no database-level uniqueness constraint
Sysco EDI 832 sync now correctly writes procurement records to MySQL — tenant context was lost during sync execution, causing ingredient matching and settings lookup to fail silently; vendor lookup also failed because sub-tenant scoping prevented finding the shared Sysco vendor
Duplicate product name validation now correctly blocks all paths (create, update, save, restore) — previously stale Redis cache could allow duplicate names through when renaming or duplicating products
Order confirmation, cancellation, and modification emails are now explicitly sent to the shared orders inbox — previously relied on the inbox having an Account Manager user record, which broke when the user was deleted
Release
4.14.0
Fixed
Bulk KPL order count now correctly shows all orders for the bulk period — previously showed fewer orders due to incorrect frontend re-filtering
Batch recipe imports no longer silently drop files — files are now processed sequentially with a 5-minute per-file timeout, preventing Gemini API overload from stalling the entire batch
Recipe import completion email now shows the number of files processed and total recipes extracted instead of empty fields
Yield field in breadcrumb sub-recipe editor is now editable in edit mode and static in view mode (was previously inverted)
Editing a component amount in breadcrumb mode now scales yield proportionally, preserving the yield-to-weight ratio
Editing yield in a breadcrumb sub-recipe no longer rescales component amounts — yield (output) and component amounts (inputs) are now independent when editing yield
Changing a sub-recipe's yield now cascades the update through all ancestor recipes in the breadcrumb chain, keeping parent quantities consistent
Component amounts now display correctly when editing sub-recipes via breadcrumb — previously clicking Edit showed raw unscaled quantities (e.g., 10,560g instead of ~171g)
Sub-recipe weights now stay correct when editing via breadcrumb navigation — previously clicking Edit reset the weight from the usage amount (e.g., 218gr) back to the recipe's default yield (e.g., 10.56kg), and Cancel did not restore it
Quantity and unit fields are now fully editable when editing nested sub-recipes via the breadcrumb editor — previously these inputs were locked in read-only mode
Swapping an ingredient in a recipe now preserves the original quantity and unit instead of resetting to the new item's defaults
Nutrition Facts Panel now displays correct values for nested sub-recipes — previously showed all zeros due to stale weight cache
Parent recipes now instantly reflect child sub-recipe changes (costing, nutrition, weights, ingredients) when navigating back via breadcrumb — cached data displays immediately with a silent background refresh
Newly created products now appear immediately in product lineup modals — previously required up to 1 hour for the cache to expire
Duplicating a meal now shows the copy instantly in the list with "(Copy)" suffix — no more 8-10 second skeleton loader wait
Production setup wizard time selectors now show full 24-hour range (12:00 AM–11:45 PM) instead of only 7 AM–7 PM, supporting early morning and late night cutoff times
Completing the production setup wizard now correctly shows the workflows area instead of re-displaying the setup prompt
Creating a new product lineup now shows instant save feedback — no more 2-3 second delay and modal flashing after clicking Save
Switching tenants no longer shows stale dashboard data from the previous tenant — cached dashboard metrics are now cleared on tenant switch
Orders page no longer incorrectly shows "Add a customer to start placing orders" banner on cold load when customers exist
Cron job login and execution failures no longer flood error monitoring — transient operational issues are now logged as warnings instead of errors
Batched notification processing no longer crashes when Redis returns pre-parsed objects instead of raw JSON strings
Sidebar customer list no longer crashes when customer data hasn't loaded yet or when customer names are missing
Orders page no longer crashes when the customer list hasn't loaded or when customers have missing names
Missing or invalid SVG icons now gracefully display a default icon instead of throwing errors to the error tracker
Release
4.13.1
Changed
FontAwesome icons are now tree-shaken — only ~150 used icons are bundled instead of all ~7,000 SVG paths, reducing main.js by ~2MB
Logout and onboarding routes are now lazy-loaded, removing them from the initial bundle
Main bundle reduced from 8.7MB to 6.6MB uncompressed (24% reduction)
Fixed
Loader now paints instantly on cold load for logged-in users — a paint-yield script ensures the browser renders the loader before V8 locks the main thread parsing Angular bundles
Product lineup creation and editing no longer fails with a 500 error when products are added — navigational properties are now stripped before persisting lineup items
Cold app load no longer shows a blank white screen for 15-20 seconds — the Angular bootstrap gate was blocked by long-lived SSE connections and heartbeat timers that kept the framework permanently "unstable"
SSE real-time events and heartbeat timers now run outside Angular's change detection zone, eliminating unnecessary rendering cycles
Frontend assets now served with proper cache headers — hashed assets get immutable 1-year cache, index.html always revalidates
Release
4.13.0
Added
Sentry performance tracing — every HTTP request now creates a transaction with child spans for MySQL, MongoDB, and outgoing HTTP calls, enabling waterfall-view diagnostics in Sentry UI
CPU profiling via @sentry/profiling-node — flamegraphs available in Sentry for traced requests (staging: 100%, production: 50% of traces)
Performance observability guide documenting Sentry traces, autocannon benchmarking, Sentry API queries, and remote profiling workflows
E2E test infrastructure for sign-up flows covering email signup and Google OAuth signup paths
Google Workspace Admin SDK helper for programmatic test user management (@getdemi.co accounts)
Backend test-mode bypass for Google OAuth — enables deterministic E2E testing without real Google sign-in (guarded: non-production only, requires shared secret)
Standalone cleanup script for orphaned test users (npx ts-node tests/scripts/cleanup-test-users.ts)
Global teardown safety net to clean up test users after suite completion
Changed
Staging backend EC2 instances upgraded from t2.small (2 GB) to t3.medium (4 GB) to match production and eliminate GC thrashing under normal load
Fixed
CodeDeploy failed deployments no longer cause 504 errors — instances are now deregistered from the ALB before PM2 is killed and re-registered after health validation passes
White screen flash (1-3 seconds) on cold app load — fonts now load asynchronously so the Demi loader paints instantly when HTML is received
Sentry captureException and captureMessage calls now include required tags object with tenant context
LOGGING_ENABLED_PROVIDERS in ecosystem.config.js was formatted as a JSON array string but parsed with comma-split, silently breaking provider activation on staging/production
Cron jobs no longer crash the process on failure — all 24 cron job onTick callbacks now properly await their async work, and a global unhandledRejection handler catches any remaining unhandled promise rejections
Webhook queue grace period handler errors no longer cause unhandled promise rejections inside setTimeout
Label ingredient listing caches no longer grow without bound — added a 500-entry cap that evicts all caches when exceeded, and fixed allAlergens never being cleared on cache eviction
Removed dead order migration observer that ran 9 unbounded full-table scans on every server restart with no effect (all migration logic was commented out)
Logout no longer throws TypeError when customer data is null, eliminating Sentry noise from .sort() on null arrays
In-flight customer data requests are now cancelled on logout, preventing stale data from overwriting cleared state
Rapid token refreshes no longer create duplicate customer initialization requests
Production backend no longer crashes hourly from memory exhaustion — staggered 10 simultaneous cron jobs across 5-minute intervals to eliminate the thundering herd at the top of each hour
Production backend no longer crashes hourly with out-of-memory errors during scheduled cron job execution — V8 heap limit raised to match available instance memory
B2B order reminder emails no longer fail for tenants without production days configured
Application no longer spikes to 2-3GB memory and enters PM2 restart loops after deployments — disabled the full TypeSense reindex on boot (47k+ records) since TypeSense Cloud persists data server-side, and gated health checks to the leader instance only
Search endpoints now return proper 400 errors instead of cryptic 500s when request data is missing or malformed
Recipe costing endpoint now returns 404 instead of 500 when a recipe doesn't exist
Kitchen production list no longer crashes when receiving empty data during sync
Order management customer and location sort no longer crashes when a name is missing
Edit order no longer crashes when product lineup loads before the order is initialized
Modern catering shop icons now handle tag names with special characters instead of triggering SVG load errors
Invalid recipe multiplier warnings are now logged at the correct severity level with diagnostic details for easier troubleshooting
Nutrition Facts Panel now correctly displays total weight for count-based serving units (e.g., "Per 2 Slice (84 g)" instead of showing only the per-unit weight of 42 g)
Audit log viewer now correctly displays audit data — queries were missing tenant scoping, causing "no data available" for all users despite data existing in MongoDB
Login errors no longer flood Sentry — handled login errors (wrong password, network issues) are now properly swallowed instead of propagating to the global error handler
Demo guard no longer crashes when user object hasn't loaded yet during route navigation
Sidebar, kitchen production list, production management, production log widget, and modern catering sidebar no longer crash when customer data is null during logout or initial load
Orders created via the public API now calculate correct pricing instead of showing $0.00 — the pricing fallback now uses recipe retail/wholesale prices when auto-calculated prices are unavailable
Application no longer shows a white screen when the backend is slow to respond — route guards now timeout after 15 seconds and redirect to a friendly error page with a retry button
Chunk load errors after deployments now show the loading spinner during automatic retries instead of a blank screen
Release
4.12.1
Fixed
UPC codes are no longer duplicated when multiple users create meals simultaneously across different servers — distributed Redis lock ensures only one server generates a UPC at a time
Sidebar navigation logo now stays pinned at the top when scrolling through long navigation menus on larger screens
Errsole "Port 8001 already in use" warning no longer fires on every PM2 worker restart — dashboard is now disabled under PM2 since it is unreachable behind the ALB
Added
Backend Sentry error tracking — server-side errors are now captured and reported to Sentry with full tenant/user context, integrated via the existing logging provider architecture
Sentry user and tenant identification — error events now include the logged-in user, tenant, and sub-tenant so issues can be traced to specific customers
Angular route-level performance tracing — page navigation timing is now captured via Sentry TraceService
CI source map uploads for both production and staging deployments — Sentry stack traces now show readable source code instead of minified bundles
Separate Sentry projects per environment — staging and production errors are tracked independently, dev environment disabled to reduce noise
Release
4.12.0
Added
Company and location settings now auto-populate timezone based on address country and province/state, removing the need for manual timezone selection
Ingredient bulk editor now includes a Status column, allowing inline status changes (Active/Inactive) without leaving the bulk editor
Quick Invite rows now have a remove button so users can delete unwanted email entries before sending
Changed
Disabled action menu items in paginated tables now use semantic <button> elements instead of <a> tags for improved accessibility
Fixed
UPC regeneration now always produces a new, unique code — replaced gap-filling algorithm with monotonically increasing (max+1) strategy, added recipe's own UPC injection to prevent self-collision, and added uniqueness verification with retry loop
Label Data tab now immediately reflects the new UPC after regeneration without requiring a page refresh
Recipe importer now shows the most recently completed import when clicking "Review imported recipes" instead of the oldest
Recipe deletion from import review no longer fails silently — error messages now include server-side details
Discarding an import now fully clears running import state, preventing ghost/zombie entries in the progress banner
Import progress banner no longer re-appears after being dismissed when the importer panel closes
File upload error messages now include the specific filename that failed and the server error detail
Upload filename collisions prevented by adding random suffix to timestamp-based file keys
Location creation and deletion no longer produce orphaned subscriptions or swallowed errors — observable chains are now properly composed
Settings forms now scroll to the first invalid field when validation fails, so users can immediately see what needs fixing
Workflow print pages no longer cut off content at the bottom — added page-break buffer spacing
Currency detection no longer defaults to CAD for all users due to a missing await on the geolocation check
Price history change badges no longer truncate in narrow columns
Ingredient bulk editor no longer gets stuck in loading state when a save fails
Release
4.11.9
Changed
V1 OpenAI invoice import code fully removed from backend — ~1,600 lines of dead code deleted from ingredient-importer service, controller, and recipe-importer API service
Ingredient validation endpoint now correctly routes to vendor-sync on port 55000 instead of port 80
Fixed
Eliminated 10-20 second white screen on cold loads and post-deployment by removing the service worker (which was being cleared and re-registered every session), unblocking the Google Maps script with async loading, and inlining the loading screen CSS for instant display of the Demi loader
Audit log viewer now displays results correctly — removed an environment name filter that was preventing logs from appearing despite data existing in MongoDB
Audit log sort order now defaults to most recent first (operationTime) instead of the base component's name field, which doesn't exist on audit log entries
Release
4.11.8
Changed
Category settings now provide instant feedback when saving edits — the updated name appears immediately without a loading spinner, with automatic rollback if the server rejects the change
Deleting a category optimistically removes it from the list immediately instead of waiting for the server response
USDA nutrition matching now uses the V2 Gemini-based matcher for improved confidence scoring and match quality
Nutrition enrichment service logs active enrichment tiers on first use for operational visibility
Fixed
KPL search now correctly narrows results when a filter is active — previously, searching within a filtered view (e.g., Portions) had no effect because filters used OR logic instead of AND logic
Products no longer disappear from all lineups when saving ingredient or NFP edits — sales associations are now only updated when the user explicitly modifies the Sales tab
DevRev knowledge base article images no longer expire after 7 days — image uploads now use permanent access_token from artifacts API instead of temporary S3 presigned URLs
Sub-recipe costing chain no longer zeros out when a single child sub-recipe has missing yield or configuration errors — parent recipes and products now show partial cost from valid children with actionable warnings identifying the broken item
Costing error messages now identify the specific sub-recipe by name instead of showing generic "Recipe has no yield" errors
Costing tab now displays structured warning cards with clickable links to broken sub-recipes, replacing the previous generic red error list
Fixed subscription feature gate bypass on recipe compliance info that was exposing compliance features to all users regardless of subscription tier
Category changes (recipe categories, ingredient categories) made by other users now automatically refresh across all connected browsers via SSE cache invalidation
Bulk editor dirty guard no longer loses track of changes after saving — removed redundant manual cache clear that interfered with change tracking
Typesense: suppressed repeated errsole email alerts for the open-food-facts collection which is intentionally unpopulated — skips recovery attempts and empty-collection warnings for this collection
USDA Typesense search: removed duplicate SR Legacy preference boost that was applied twice during candidate deduplication
Nutrition refresh cron no longer overwrites high-confidence matches (≥0.7) with lower-confidence results during scheduled refreshes
Ingredients without USDA matches are now marked to prevent repeated failed re-processing on every cron run
Release
4.11.7
Added
USDA nutrition cron now supports a tenantId parameter to scope processing to a single tenant's ingredients, preventing cross-tenant data changes during targeted refreshes
USDA nutrition parser now extracts weightToEaRatio (grams per "each" unit) from USDA food portion data, enabling accurate per-unit weight conversions for produce and countable items
USDA nutrition enrichment now falls back to the USDA FDC REST API when food data is not available in local MongoDB — unblocks enrichment for tenants whose FDC IDs haven't been bulk-imported
Audit log diagnostics endpoint (GET /audit-logs/diagnostics) — returns document counts per environment, redacted MongoDB host, and database name to help diagnose missing audit data
MongoDB startup connection logging — logs redacted host and database name on application boot for PM2-level visibility
PostHog signup funnel: event tracking for signup form submissions, successes, failures, and duplicate accounts to diagnose drop-off points
Changed
Header redesign: dark green branded header band using Demi brand color across sidebar logo and topbar for stronger visual hierarchy
Tenant and sub-tenant switchers restyle as compact rounded pills with building icon and floating dropdown panels
Notification bell and help center icons simplified to borderless style; create (+) button keeps bordered icon pattern
Notification badge redesign: minimal red dot for counts under 10, compact pill badge for 10+
Fixed
Billing: upgrading a subscription mid-trial now correctly ends the trial immediately, charges the customer, and activates the paid plan — previously the trial continued and no charge was created until the trial expired
Billing: payment failures (declined card, 3D Secure required) after ending a trial are now detected and surfaced to the user instead of showing a false success message
Billing: concurrent subscription/customer creation requests can no longer produce duplicate Stripe resources (added idempotency keys)
Billing: webhook events for old/canceled subscriptions no longer overwrite active subscription data on the tenant
Billing: non-card payment methods (ACH, bank debit) are now correctly recognized when checking for a default payment method
Billing: plan details page no longer crashes when the plan object hasn't loaded yet (null dereference fix)
Billing: plan name in the upgrade confirmation dialog now shows the actual plan name instead of "current plan"
Billing: currency toggle on pricing page no longer re-enables for tenants with an active subscription locked to a specific currency
Costing: Calgary (separate database) costing data packs no longer get contaminated with main production data during concurrent cache re-warming — replaced unsafe singleton datasource mutation with isolated repository instances
Mobile signup: Google OAuth redirect no longer shows a blank login page — added a "Signing you in..." spinner with a 15-second safety timeout that falls back to the login form
Mobile signup: page no longer overflows behind the mobile browser toolbar — replaced Bootstrap vh-100 with dynamic viewport height (100dvh)
Mobile signup: in-app browsers (Instagram, Google Search App) that block reCAPTCHA no longer show a permanent "Security verification failed" error — signup proceeds without the token since the backend accepts it
Login: OAuth redirect safety timeout now fires even when the redirect block is entered but navigation fails (guard rejection, 401s) — previously the 15s fallback was unreachable due to an early return
Signup: page no longer shows blank white screen on first navigation after logout — form now displays while auth service is still initializing
Tenant switcher: removed signupFlowComplete filtering that could hide valid tenants from the switcher dropdown
QBO sync error notification email: customer link no longer renders as "undefined/customers/123" when the tenant base URL is unavailable
Notification badge: count now clears correctly after dismissing all notifications — previously the badge persisted because dismissed notifications weren't marked as seen
Tag selector (chip control): clicking a tag option now registers reliably — previously the blur event could destroy the dropdown before the selection event fired
Location settings: added French translation toggle for Canadian locations, matching the existing company-level setting
Search/filter: Typesense collections no longer randomly become empty — schema updates now use non-destructive migration instead of dropping and recreating collections, concurrent sync operations are prevented via distributed locking, and health check recovery skips collections that are actively syncing
Global Admin: can now edit a user's role — previously the role dropdown was hidden when operating in Global Admin mode
Global Admin: can now switch into any company from the Companies page — previously failed silently because the system required a direct user-tenant record
Global Admins can now add users to any tenant — previously blocked with "You do not have access to this tenant" when the admin wasn't a member of the target tenant
Tenant switcher: legacy tenants (created before the signup flow feature) now appear in the dropdown — previously hidden due to a missing flag being treated as false
Settings → Locations: refresh button now updates team member counts instead of showing stale cached data
Team invitations: deleting an email address no longer causes a "Failed to send invitations" error from orphaned role selection
Ingredient deletion undo: clicking "Undo" after deleting an ingredient now shows an error message if the restore fails, instead of silently leaving a blank results page
Recipe import notifications: clicking the "import complete" notification now correctly opens the recipe importer review instead of navigating to a deprecated route
Financial reports: shared library ingredients and recipes are now included in all report calculations — previously excluded by tenant isolation when referenced from tenant-specific products
Recipe Importer review: entering edit mode no longer immediately triggers the unsaved-changes guard — the dirty flag is now calculated from actual data changes instead of being set unconditionally on edit activation
Recipe editor: "Used In" section (meals and sub-recipes) no longer clears after saving — metadata is now re-fetched fresh on every save instead of serving stale cached data
Verified Library ingredients: Global Admin updates no longer fail on first attempt — library items are now updated directly instead of being routed through the tenant versioning system
Ingredients list: category name now updates immediately after saving an ingredient with a changed category — previously the label stayed stale until a full page refresh
Recipe importer: deleting a recipe during import review no longer leaks its ingredients into the tenant library — finalization now only promotes ingredients still linked to surviving recipes
Recipe importer: review warning badges no longer disappear after page refresh — recipe status now correctly reflects whether child components still need review
Recipe importer: sub-recipes and ingredients no longer disappear after finalizing imports — status activation cascade was broken by a key mismatch between pre-save and post-save hooks
Recipe importer: valid tenants no longer see "Subscription Required" errors during large imports — temporary staging recipes are now excluded from subscription recipe counts
Recipe editor: saving recipes with unresolved or duplicate ingredients no longer drops other ingredients — component matching now uses row identity instead of ingredient/recipe foreign keys
Recipe editor: changing or regenerating a UPC no longer removes the product from all product lineups — a race condition in the save flow could send empty sales associations, causing a destructive diff that deleted all lineup assignments
Accounting role users can now access the Settings page to manage billing — previously blocked by the route guard despite having the ManageBilling permission
Team member deletion now properly removes the user from the tenant instead of silently setting them to inactive, which left ghost records in the system
Deleting a user from the admin panel now shows an error toast if the pre-delete verification fails, instead of silently doing nothing
User creation: success toast now appears once after API completes ("User created successfully") instead of showing a premature "User saved" toast followed by a delayed duplicate
User creation: role, customer, and location assignments now persist after saving — previously the readonly view showed empty assignments because the router skipped reloading the full user with relations
Users list: refresh button now properly clears the store cache and reloads data instead of silently failing
Users list: 3-dot menu no longer shows a legacy "Edit" action that opened an outdated full-form view — "View" now opens the modern section-based UI with inline editing
Audit log page no longer shows a stuck "Loading more..." message when there are no results — now displays a proper loading spinner during fetch and an empty state message when no logs match the selected filters
Audit log infinite scroll no longer fires requests when all items have been loaded
Rapid filter changes on the audit log page no longer produce duplicate entries — pending API requests are cancelled before resetting the list
Audit log queries now always include a date range constraint (7-day fallback) to prevent unbounded MongoDB scans
Product pricing: default prices now correctly use wholesale price for wholesale customers and retail price for retail customers — previously all customers saw retail prices regardless of their sales type
Product pricing: adding a product without entering a custom price no longer saves an undefined value — now falls back to the correct default price for the customer's sales type
Order placement: "Place order" button now shows instant success feedback and switches to readonly confirmation view — prevents duplicate orders caused by re-clicking during slow API response
Create product modal now opens instantly instead of waiting 3-5 seconds for Typesense search method to resolve
USDA nutrition cron no longer gets stuck in an infinite retry loop on ingredients where the FDC ID returns 404 from the USDA API — error records now get stamped to allow the cron to advance
USDA nutrition search now correctly matches ingredients named in taxonomy format (e.g., "Herbs, Sage") — comma-separated names are inverted before searching to put the specific term first
Release
4.10.7
Added
Unit test coverage for critical backend services (costing calculations, cache invalidation, Typesense search merging) and frontend services (Typesense search, ingredient/recipe/product stores)
Fixed
Bulk KPL child component rows now display correct units when parent "Needed" and "To Make" amounts differ in scale — previously showed "5.4 ml" instead of "5.4 L" for large production quantities
Bulk KPL inventory-only recipes (with production demand but no orders) now show correct child component amounts instead of near-zero values
Bulk KPL now shows all portion recipes with production demand, even those without active orders in the current period
"Print packing slip" from the order 3-dots menu now correctly generates the packing slip instead of printing a blank page
Bulk KPL no longer shows missing items for tenants with NULL subTenantId — duplicate production day records caused by a MySQL unique index gap are now detected, consolidated, and cleaned up automatically
Costing calculations for tenants with dedicated databases (Calgary) now query the correct database — previously, cost recalculations after cache invalidation would query the shared database, returning wrong or missing recipe/ingredient data
Recipe ingredient dropdown now works for Calgary tenant — falls back to MySQL store search when Typesense is unavailable due to separate RDS with colliding document IDs
Release
4.10.6
Added
/health endpoint that verifies main MySQL and all tenant database connectivity — returns 503 when any database is unreachable, enabling ALB to deregister unhealthy instances
CodeDeploy post-deploy validation — new instances must pass PM2 process check and /health HTTP 200 before deployment is marked successful; failures trigger automatic rollback
Structured logging for tenant database failures (TENANT_DB_FAILURE) and schema drift (TENANT_SCHEMA_DRIFT) for CloudWatch alerting
MongoDB Atlas VPC peering for production and staging — EC2 instances now connect to Atlas over private networking, eliminating the need to whitelist individual IPs and preventing 502 errors when ASG scales
PostHog session replay, heatmaps, and automatic pageview/pageleave tracking via SDK config
Custom PostHog event tracking for 12 key business actions: order creation/approval, recipe creation/save, invoice QBO sync, report views, onboarding funnel steps, modern catering checkout/cart, ingredient creation, and customer creation
Ingredients, recipes, and products can now be found by typing their numeric ID in the search bar — works in both the global search, paginated table views, and the component picker dropdown when editing a recipe
Recipe component picker dropdown now shows an alphabetical browse list immediately when opened, matching the previous behavior
Nightly materialized view reconciliation cron job that detects and repairs count drift between source collections and denormalized views across all tenants
SSE heartbeat detection — frontend now detects stale server connections (3 missed heartbeats) and automatically invalidates all cache layers to prevent serving outdated data
Background tab freshness check — returning to a tab after 5+ minutes in the background triggers a full cache refresh across all layers
Catering cache now connected to SSE invalidation so menu and order changes propagate in real-time
Changed
Recipe ingredient/sub-recipe "Type to add item" dropdown now uses Typesense search instead of loading all items upfront — faster initial load, server-side relevance ranking, and correct multi-tenant hierarchy merging that no longer hides active ingredients when related copies are deleted at other tenant levels
Fixed
Fresh ASG instances no longer crash-loop due to missing PM2 environment variables — PM2_HOME is now explicitly set in all CodeDeploy start scripts
Selecting an item from the recipe component picker dropdown is now instant — uses existing store cache instead of redundant re-fetching, and properly clears the search text after selection
Ingredient cost changes now immediately propagate through the costing chain (ingredient → sub-recipe → product) without requiring a manual cache clear — previously, editing a procurement cost left stale prices in sub-recipes and products until the 24-hour cache expired
Packing summary export and packing slips now always use fresh data instead of serving stale results from a 1-hour cache
Creating a new recipe, product, or ingredient no longer shows empty content after saving — components, costing, and procurement data now display immediately without needing to close and reopen
Typesense collections no longer stay stale after deployments — stale lock files from interrupted reindexes are now detected and cleaned up automatically
Packing summary Excel export no longer shows incorrect AM/PM times (e.g., 11:30 AM was exported as 00:30)
Packing operations now show error toasts on failure instead of silently failing or leaving the UI in a loading state
Recipe component picker dropdown now highlights the first result correctly — pressing Enter always selects the top match instead of the second item
Typing in quantity/unit fields no longer loses focus when background cost enrichment completes while editing
Ingredient import progress bar no longer displays excessive decimal places (e.g., 81.11489660804142%) — percentage now rounds to a whole number
IndexedDB race condition on tenant switch — concurrent deleteAll() calls no longer corrupt the database or allow stale writes during the delete window
Cache thundering herd — TTL jitter (up to 5 minutes) now staggers expiration across backend repository caches and frontend stores, preventing simultaneous revalidation spikes
HTTP interceptor stale cache entries now self-evict after 2 consecutive background revalidation failures instead of serving stale data indefinitely
In-memory store clearCache() now cancels pending API requests to prevent cleared caches from being repopulated by in-flight responses
TypeSense search cache now uses true LRU eviction (1 oldest entry) instead of bulk-deleting 20 entries when hitting the cap
recentlySavedIds in management services now capped at 500 entries with FIFO eviction to prevent unbounded memory growth
Materialized view cron job distributed lock now only releases when actually acquired, preventing accidental release of another instance's lock
Startup config validator that prevents the app from booting if connected to the wrong MongoDB cluster or missing critical environment variables — catches ASG config drift before it causes data issues
PM2 crash loop prevention with max_restarts: 5 and min_uptime: 10s — stops infinite restart loops from exhausting resources
Infrastructure resilience plan documenting phased approach to preventing config drift incidents
Unit tests for sign-up flow: Global Admin fallthrough, OTP verification bypass, token encoding with signup flag, and onboarding routing after password creation
Release
4.10.5
Fixed
Bulk workflow sub-recipe ingredient amounts now display correctly when inventory data triggers unit auto-scaling (e.g., grams to kilograms) — previously showed drastically smaller quantities due to a unit mismatch in child component multiplier calculations
Recipe importer no longer hangs at 2% progress — stream event ordering is now guaranteed, empty AI responses are detected with clear error messages, and import run tracking is more resilient
New sign-ups on app.getdemi.co no longer receive a "Global Admin permissions" error when creating their password
Email verification no longer fails for new sign-ups due to tenant-scoped database queries
New sign-ups now go directly to the onboarding flow after creating their password instead of being kicked to the login screen
Release
4.10.4
Fixed
Price history view no longer regresses to stale data after saving — all history reloads are frozen for 15 seconds post-save while the backend audit trail commits, eliminating the race condition permanently
Release
4.10.3
Fixed
Price history no longer briefly shows incorrect data after saving — the UI now retries fetching audit trail data instead of overwriting the optimistic display with stale API responses
Multiple rapid price saves on the same ingredient no longer interfere with each other — each save's optimistic entry is tracked independently
Release
4.10.2
Fixed
New user sign-ups no longer stall on the workspace setup screen — verification emails now link to the correct domain and tenant resolution uses the correct fallback order
Onboarding setup failures now show an error screen with a "Try again" button instead of an infinite loading spinner
Expired verification links now show a friendly "Your link has expired" message instead of a raw HTTP error
Verification email links no longer break when SendGrid click tracking encodes special characters
Onboarding workspace setup no longer times out on the first save — Stripe syncing now happens in the background
Team member assignment and final onboarding steps no longer fail for new sign-ups due to missing tenant context
Dashboard now shows "Welcome, {name}!" instead of "Welcome, there!" after completing onboarding
Dashboard no longer gets stuck on skeleton loaders after a production deployment — data loading now waits for authentication to complete before fetching widget data
Price History tab no longer flashes "Loading..." and briefly shows incorrect data after saving a cost change — the optimistic entry now stays stable until the server confirms the update
Added
"Didn't receive the email? Resend" link on the sign-up verification screen with a 30-second cooldown
Inline password requirements shown during password creation
Helper text on the sign-up form explaining what happens after registration
Changed
Nutrition Facts Panel tabs now default to the subtenant's country — Canadian locations see CFIA first, US and other locations see FDA first
Password creation page title updated from "Login | Demi" to "Create Password | Demi"
Password fields now include proper accessibility labels and autocomplete="new-password" attributes
Sensitive OTP parameters are stripped from the URL after successful password creation
Password strength is now shown as a visual meter with Weak/Fair/Good/Strong levels instead of text-only feedback
Release
4.10.1
Added
"Number of Items" column in the Orders list now shows total item quantity per order when using Typesense search, matching the column already available in legacy search mode
Fixed
Price History tab now reliably shows optimistic price updates during long-running saves — previously, a race condition allowed stale history data to overwrite the UI before the save completed
Users with stale cached sessions after a deployment no longer see "Unable to connect" errors or MFA rejection — the login page now auto-detects version mismatches and refreshes before login
Release
4.9.9
Added
Google Analytics (GA4) conversion tracking on signup — enables Google Ads to optimize campaigns against actual registrations
PostHog analytics now identifies users and associates them with their tenant and location, enabling per-company and per-location usage insights
Typesense collections that become empty at runtime (due to schema migration, failed sync, or manual deletion) are now automatically detected and recovered within 5 minutes — previously required an app restart or manual reindex
Nightly Typesense sync now fires a critical alert if any collection remains empty after sync completes, enabling faster incident response
Errsole now supports email notifications for fatal-level alerts via SMTP (configured with ERRSOLE_SMTP_* env vars)
Fixed
OAuth email lookup now escapes special regex characters, preventing potential account matching issues with emails containing +, ., or other special characters
Signup failure now properly cleans up orphaned tenants and user-tenant records — previously only the user was deleted, leaving orphaned data
QBO invoice sync now correctly assigns per-tenant DepartmentRef (Location) — previously, cross-tenant settings pollution caused one tenant's QBO location to overwrite another's, resulting in missing or incorrect Location mapping
Login no longer breaks for the rest of the session after a single failed attempt — a critical bug caused the auth stream to permanently shut down on any error
Raw HTTP error messages (e.g., "Http failure response for...504 Gateway Timeout") are now replaced with user-friendly messages on the login and MFA pages
"Code sent successfully" no longer appears when OTP resend actually failed (e.g., due to a server timeout)
MFA code inputs now only accept numeric digits — letters, symbols, and non-numeric paste content are automatically stripped
OTP resend button now has a 30-second cooldown to prevent accidental spam
Navigating directly to the MFA page without credentials no longer shows a confusing "Credentials not provided" error — users are redirected to the login page
"Tenant context is required" error toast no longer appears on dashboard load when sub-tenant has not yet been selected
Saving an ingredient no longer causes a secondary visual flash, broken Edit button, lost price changes, or disappearing "Used in" data — eliminated a redundant post-save API reload that replaced the item with incomplete data
"Unable to load ingredient" errors no longer appear when closing and reopening an ingredient shortly after saving
Ingredient and recipe saves are now faster — eliminated redundant duplicate database writes during save-with-items operations
Ingredient cost changes now persist correctly after saving — a legacy price-history loop was stripping the procurement ID, causing an INSERT (duplicate) instead of UPDATE
Ingredient price history now records cost changes made via the editor and bulk importer — a bulk database update was silently suppressing audit log entries for price changes
Price History tab now shows the latest cost change immediately after saving — previously showed stale data due to audit log write timing and navigation cancelling the history refresh
Saving an ingredient with a cost change no longer reverts to the old price on refresh — a missing default-enforcement step allowed stale procurement defaults to persist
Changed
MongoDB infrastructure documentation updated to reflect dedicated cluster architecture (separate staging and production clusters)
Release
4.9.8
Fixed
Removed unnecessary "Some data may load slower than expected" warning toast that appeared on dashboard load when the sales dataset preload failed — the dashboard loads correctly via API fallback and this toast was just noise
Saving a recipe after changing a component weight no longer shows a delayed "Failed to save" error toast — product cache invalidation was executing as an unawaited promise, causing errors to surface 10-15 seconds after the save had already succeeded
Release
4.9.7
Fixed
Clearing a UPC and saving now correctly persists the empty value — previously the backend silently regenerated a new UPC
UPC section on recipe edit now shows a barcode visual with copy-to-clipboard, matching the label data tab
Added one-click "Regenerate" button to replace a UPC without clearing and re-saving
Bulk KPL category filter dropdown no longer shows categories for recipes with zero consumption — only categories with visible rows appear
UPC code now updates visually after clearing and saving — backend-generated UPC is fetched once the optimistic save completes
UPC edit field now correctly returns to read-only display after save-and-stay
UPC editing state resets properly when navigating between recipes
Saved changes now persist immediately when re-opening items — resolved stale data issue across all cache tiers (in-memory, IndexedDB, HTTP) that caused old values to appear after edits
Quick single-character edits now correctly trigger the save guard instead of being silently lost
IndexedDB no longer fails on entities with non-numeric IDs, preventing persistent stale cache entries
Deleting an order after cancelling it no longer shows "operation already in progress" — the cancel→delete workflow now works seamlessly
Deleted orders are now removed from the list immediately without requiring a manual refresh
Orders page no longer shows a duplicate refresh icon when using Typesense search
Release
4.9.6
Fixed
"Refresh" button on version update banner now properly clears service worker cache before reloading, ensuring users get the latest version immediately
CloudFront cache invalidation in deploy workflows no longer blocks deployment on transient failures
Release
4.9.5
Added
Orders page now uses Typesense-powered full-text search with faceted filtering for status, order type, and customer — search results are faster and more relevant than the previous API-based search
Fixed
Filter dropdown checkboxes no longer get distorted when items have long names
Column sorting now works correctly for Customer, Total, and Status columns on the orders page
Search no longer sends invalid queries when searching by non-numeric terms (e.g., customer names)
Recipes (and other entities) now correctly show saved edits when re-opened after saving — previously, stale cached data could appear, making edits look "reverted"
Rapid consecutive saves to the same item are now serialized to prevent out-of-order writes that could cause data loss
Dirty guard ("unsaved changes" prompt) now reliably detects edits to products and recipes after saving — previously, clearing store caches on save caused async data to load after the baseline was captured, making the guard silently fail on subsequent edits
Order cancel, delete, and approve actions now update the UI instantly without requiring a page refresh
Removed false maintenance mode lockouts caused by stale cached global settings
Orders refresh button now works correctly in Typesense mode
Release
4.9.3
Fixed
Users no longer sporadically see "maintenance mode" after login or navigation — the HTTP cache interceptor was serving cross-tenant global settings responses, causing false maintenance mode triggers
Recipe and product costing no longer reverts to stale values after saving — cache is now cleared synchronously before the costing tab reloads
Order collection reindex in Typesense no longer fails on missing or malformed order data
Changed
Added unit tests for duplicate-safe production day record creation guards
Release
4.9.2
Fixed
Recipe and product costing no longer shows $0.00 after saving — costing cache pre-seed now reads fresh data instead of racing against background cache updates
Bulk Workflow no longer returns stale or incomplete data when duplicate production day records exist — duplicate records are cleaned up during migration, a unique constraint prevents future duplicates, and queries always prefer the most recent record
QBO invoice sync now sets correct Location (DepartmentRef) for automated sync paths (cron/webhooks)
Ingredient importer review links now filter to the specific import run instead of showing all ingredients
Release
4.9.1
Added
Real-time data synchronization via Server-Sent Events (SSE) — data changes on any API instance are pushed to all connected browsers within 1-2 seconds
Redis Pub/Sub event bus for cross-instance data change broadcasting across all 3 API instances
MongoDB Change Streams for automatic mutation detection on source collections
Write-through Redis data packs for all major entities — user reads serve from Redis cache, not MongoDB
Login-triggered cache pre-warming with distributed locking to prevent thundering herd
IndexedDB persistence layer (via Dexie.js) — browser retains data across page refreshes for instant navigation
3-tier cache lookup: in-memory → IndexedDB → API for near-instant data access
Optimistic UI framework with automatic snapshot/rollback — CRUD operations update UI instantly with background API calls
Nightly reconciliation process for materialized view drift detection and repair
Angular Service Worker for static asset caching — app shell loads instantly from cache
HTTP cache interceptor with stale-while-revalidate strategy for GET API responses
Open Food Facts database integration for broader international nutrition matching — complements USDA database with crowdsourced product data from 3M+ products worldwide (feature flag controlled, admin import required)
Changed
Ingredient Importer V2: progress bar now smoothly animates during long AI extraction and commit phases instead of stalling at fixed percentages
Ingredient Importer: vendor filter dropdown now shows each vendor as a separate filter option instead of combining multiple vendors into a single entry
Ingredient detail quantity label now shows the shelf unit (ea, lb, oz) instead of repeating the purchase unit (Case)
Cache invalidation now publishes events to EventBus for real-time propagation to all instances and browsers
Costing tab now loads in under 1 second after saving a recipe with ingredients/sub-recipes — data packs are preserved during junction table changes and the recipe cost is pre-seeded in the background
Cache invalidation for costing is now surgical per model — vendor, container, and packaging changes no longer clear data packs unnecessarily
Individual recipe/ingredient changes patch the cached data pack in-place instead of clearing the entire cache
API Developer Portal redesigned with Demi brand identity — Inter typography, brand color tokens, inline SVG logo, FontAwesome icons, unified header/footer, sidebar navigation, breadcrumbs, and copy-to-clipboard on all code blocks
Ingredient import upload screen now leads with AI branding — clearer copy, Demi AI eyebrow badge with sparkle icon, and subtle ambient particle animation in the header
Ingredient Importer V2: original filename is now passed to the AI service for more reliable vendor name inference
Fixed
Bulk Workflow (multi-date KPL) now shows all recipe types — sauces, dressings, spreads, portions, and components that were previously missing from the Bulk view now appear correctly
Bulk Workflow no longer displays items with zero usage across all selected dates — applies to parent recipes, child ingredients, sub-recipes, and bulk component rows
QBO invoices now receive the correct Location (Department) for each kitchen/facility — previously all invoices received the same location after the multi-tenant database migration
Ingredient Importer V2: imports no longer crash or get stuck during deployment — graceful shutdown with SIGTERM-first, automatic recovery of stuck tasks on boot, and per-item shutdown detection during the commit phase
Ingredient Importer V2: large imports (100+ items) no longer cause memory pressure — commit phase now processes in batches of 20 with brief yields for background sync, and nutrition enrichment is deferred 30 seconds after commit completes
Ingredient Importer V2: users now receive email notifications when an import completes or fails — includes import summary stats, vendor name, and a direct link to review imported items
Ingredient Importer V2: vendor name now correctly resolves during re-import (was stuck on "Unknown Vendor" due to race condition between stream message processing and result handling)
Ingredient Importer V2: "Finish and review" now filters to show only items from the current import instead of the entire ingredient list
Ingredient Importer V2: added 5-minute stream timeout to prevent XLSX imports from hanging indefinitely
Ingredient Importer V2: success screen now appears immediately after import completes — post-import work (email notification, nutrition enrichment) no longer blocks the loading screen for 10+ minutes on large imports
Ingredient Importer V2: categories now update when re-importing items that were previously assigned to "Uncategorized" — previously only "Ingredients" was treated as overridable
Ingredient Importer V2: new ingredients created via import now receive a recipe usage unit (g, ml, or ea) based on their shelf unit type — previously left blank, causing issues in recipe costing
Ingredient Importer V2: AI name formatting now preserves "Canned", "Jarred", and "Frozen" as storage format attributes instead of stripping them as packaging codes
Ingredient detail no longer shows a false "volume/weight mismatch" warning banner for liquid ingredients with weight-based USDA nutrition data
Nutrition Facts Panel on ingredient detail now correctly displays "Per 100 g" based on the nutrition data's storage type, instead of incorrectly showing "Per 100 ml (100 g)" when the recipe usage unit is a volume unit
KPL cache pre-warm cron no longer creates one log entry per tenant per tick — reduced from ~1,200 entries/hour to ~12 entries/hour
TypeSense nightly sync no longer reports false-positive "success" when 0 entities are synced — now correctly flags as failure to surface datasource issues
KPL Cache Pre-Warm and Dashboard Reports Warm cron jobs are now visible in the admin cron dashboard for monitoring and manual triggering
Ingredient nutrition facts panel now correctly displays the custom serving size unit after saving and reloading — unit selection now persists instead of reverting to defaults
Orders search reindexing no longer fails completely when individual orders have missing customer data — reindex completes successfully and logs specific problematic orders for review
Order search reindex no longer fails — added missing schema fields (orderNotes, customerLocationId, currency) that caused Typesense to reject every order document during reindex
Release
4.8.1
Changed
Dashboard access now restricted to management roles (Owner, Admin, Manager, Accounting, Kitchen Manager, Account Manager) — other roles are redirected to their primary workspace on login
Ingredient importer processing and result screens redesigned — replaces progress ring with animated spiral loader, adds smooth progress bar with status text tracking, and streamlines success/failure/issues result screens with clearer messaging and status cards
Fixed
User role changes now appear immediately in the user profile without requiring a page refresh
Removing multiple tenants from a user can now be done in a single action instead of reopening the modal for each removal
Sales and Team Member roles now correctly route to /orders on login instead of being incorrectly routed to /sub-recipes
Dashboard now automatically reloads when switching tenants or subtenants — no more stale data from previous tenant
Dashboard subscription cleanup now properly handles persistent and ephemeral subscriptions — fixes memory leak where tenant change subscriptions were immediately unsubscribed
Dashboard now shows user-facing error notifications when data fails to load instead of silently failing
Ingredient nutrition facts panel now correctly saves and restores the custom serving size unit (e.g., "3 oz") — previously only the size was saved, causing the unit to reset to defaults when reopening ingredients
Dashboard now displays correct tenant data immediately after switching tenants — cached data is cleared to force fresh data fetch
Typesense service now uses graceful fallback with warnings instead of crashing when search-only API key is missing in production
Cheftec Recipe importer no longer gets stuck at 5% during library pre-fetch — batches now timeout individually after 60 seconds instead of hanging indefinitely
Cheftec Recipe importer no longer gets auto-failed during post-import enrichment on large imports (2000+ recipes) — progress updates now keep the task alive throughout all enrichment steps
Resolved random maintenance-mode lockouts that blocked users after deployments and tenant switches — caused by cross-tenant settings contamination from an unauthenticated API endpoint
Release
4.8.0
Added
Global "Refresh" button on the dashboard toolbar — one click refreshes all widgets without showing skeleton loaders, keeping existing data visible while new data loads
KPI card values animate with a smooth count-up effect and teal flash when data changes after a refresh
Product changes (create, edit, delete) now automatically invalidate KPL cache — production workflows reflect updated product names and data within ~15 seconds
Changed
Dashboard reports now load significantly faster by only fetching recipes, ingredients, and units that are actually used in the requested data, rather than loading the entire database — reduces memory usage by ~90% and cuts cold-cache load times from 60s to 10-15s
Navigating to an uncached production date in any KPL workflow (Daily Prep, Combining, Bulk, Production, Packing) now returns immediately with a progress indicator instead of blocking for 60+ seconds while data generates
Dashboard date range changes are now instant (<100ms) for ranges within the last 90 days — switching between "7 Days", "30 Days", or "90 Days" no longer triggers API calls
Dashboard "12 Months" quick pick replaced with "90 Days" — all preset buttons now respond instantly
Dashboard now loads in ~2-3 seconds instead of ~24 seconds — report results are cached server-side in Redis (24h) and pre-warmed daily at 5am PST via scheduled job
Dashboard widgets now load in parallel tracks instead of sequential waves — reducing user-perceived load time from 36s to ~5s (86% faster)
Dashboard data survives browser refresh — cached in sessionStorage so F5 no longer triggers a full reload
Dashboard in-memory cache extended from 5 minutes to 60 minutes for faster back-navigation
Data changes (new orders, ingredient updates, recipe edits, product updates) automatically refresh dashboard cache in the background within ~5 seconds — no manual refresh needed
Modern Catering module restyled to use Untitled UI design tokens for consistent spacing, typography, colors, and shadows across the platform
Fixed
Dashboard widgets no longer briefly flash "no data found" messages while data is still loading — skeleton loaders now persist until data fully arrives
Dashboard KPI card headers no longer disappear and suddenly reappear during browser refresh (F5) — skeleton loaders now show briefly instead of causing jarring visual jumps
Dashboard KPI values now animate smoothly from 0 on initial page load (new browser session) — count-up animation previously only played during manual refreshes
Sidebar KPL links now preserve the last-viewed production date when navigating away and back — no longer resets to today's date
Corrupted production day dates (e.g. containing URL query strings) are now auto-corrected on read, preventing console errors in the date picker
Production date picker loads ~90% faster — excluded unused large data columns from the production-days API response, reducing payload from several MB to ~20KB
Navigating to future scheduled production dates no longer silently redirects to a different date — the KPL now correctly stays on the selected date and triggers data generation via the progress indicator
Kitchen Production List now persists the selected production date when navigating away and back — no longer auto-advances to the next production day
Bulk Workflow and Daily Prep Workflow date loading is now ~93% faster (~2-3 seconds instead of ~45 seconds for 30-day ranges) — dates are processed in parallel instead of sequentially
Dashboard Ingredient Usage widget now correctly converts between weight and "each" units (e.g., Cucumber sold in grams but procured by each) using the ingredient's eaWeightGrams bridge — previously showed raw gram amounts instead of shelf-unit quantities
Backend no longer enters infinite crash loop when Errsole dashboard port is held by a previous PM2 worker — detects port conflict and disables dashboard gracefully while keeping log collection active
Release
4.7.0
Added
Daily Prep workflow (KPL section) now includes a production preview panel showing finished goods that will be created from the selected components — helps operators understand what recipes they're building toward
KPL Calendar now shows weekly cutoff information (e.g., "Weekly Cutoff: Wednesday at 12:00 PM") instead of generic "[N] days before" text — clearer for operators planning production
Dashboard reports now include date range selector with quick picks (7 Days, 30 Days, 90 Days) — allows analysis of specific time periods instead of just "all time"
Changed
Dashboard report loading now shows skeleton loaders instead of blank state while data loads — better perceived performance
KPL combining workflow component selection step now groups components by recipe — easier to find specific items when working with large production days
Production schedule settings UI redesigned with tab-based navigation for Execution (daily) vs Bulk production day types
Fixed
Dashboard revenue KPI now displays correct total revenue instead of showing only the sum of invoiced revenue — uninvoiced orders are now included in the total
Product selection dropdowns in Order entry now render instantly (~50ms) instead of hanging for 8-12 seconds — optimized query removed duplicate join that loaded product stock for every product row
Order entry now properly saves customer email addresses — issue was limited to orders created through the "Create Order" flow (not recurring orders)
"Refresh USDA Data" admin cron job (Nutrition Matcher v2) now correctly escapes quotes in ingredient names before sending to OpenAI — previously failed on names containing single quotes like "Bob's Red Mill"
"Refresh USDA Data" admin cron job now chunks OpenAI requests to 20 parallel calls instead of processing all ingredients sequentially — this reduces runtime by 90% for large batches
"Refresh USDA Data" admin cron job now retries failed OpenAI calls with exponential backoff instead of silently skipping them — ensures all ingredients get processed even during rate limiting
KPL production day records now correctly deduplicate by composite key (type + day) instead of overwriting with latest entry — fixes issue where creating multiple production days of same type would result in only one record being saved
Refreshing a component in the KPL editor while in "View Mode" no longer crashes the app — the editor now correctly switches back to editing when refresh is triggered
KPL component edit modal no longer hangs indefinitely when saving changes — refreshes component data after save completes, and shows error if save fails
KPL calendar now displays correct weekly cutoff information — shows "Weekly Cutoff: [Day] at [Time]" instead of incorrectly showing "[N] days before"
Combining Worksheet ingredients now sort alphabetically by name for easier scanning — previously sorted by database ID (random order)
Release
4.6.0
Added
Invoice Importer V2 using Gemini vision extraction and smart matching for improved accuracy when importing vendor invoices
Office document format support (.xls) added to recipe and ingredient importers
Fixed
TypeSense search results are now refreshed immediately after deployments instead of waiting up to 24 hours for the nightly sync
Nightly TypeSense sync now runs at 2am PST instead of 2am UTC (which was 6pm PST)
Save guard now correctly detects unsaved changes when swapping, removing, or changing amounts of recipe ingredients — previously these changes could be silently dropped during a brief detection window after undo/redo/save operations
Product/Sub-Recipe Costing tab price changes now trigger the save guard, preventing users from losing unsaved pricing edits when navigating away
Recipe and ingredient status dropdown changes (Active/Inactive/Draft) now trigger the save guard, preventing users from losing unsaved status edits when navigating away
Release
4.5.0
Added
Comprehensive backend unit test audit covering customer relations, ingredient tenant versioning, meal plan orders, meeting orders, shopping cart, entity conversion, and QBO sync services
Comprehensive unit tests for Edit Ingredient procurement selector functionality including DOM thrashing prevention, option caching, and state management
TypeSense search now available on management pages (Customers, Orders, Users) for faster filtering and lookup
Smart Yield Engine with USDA Handbook 102 data for accurate cooking yield calculations across ingredients
Changed
Revamped user edit layout and style
User edit page now supports section-level inline editing, allowing updates to individual sections (profile info, role, tenants) without entering full edit mode
Changed
MC: Significantly improved performance for meal plan and meeting order processing - batch operations now complete in minutes instead of hours
MC: Reduced page load times with intelligent API response caching for meal plan data, product lineups, and tag categories
Fixed
Bulk worklist now correctly includes same-day delivery items in portion category counts and component rows — previously missing items like "Miso 2 oz" and "Shug" that had deliveries on the production day
Bulk worklist multi-date view no longer shows zero-amount rows when filtering by date range — recipe, organizing, and customer views now properly filter out items with no demand across all selected dates
Recipe Costing tab no longer gets stuck in loading state when clicking Edit immediately after opening the tab
Ingredient cost calculation now correctly handles "each" type pack sizes (each, box, bag, dozen, etc.) when recipe usage unit is weight-based — previously showing costs up to 50x higher than actual (e.g., $433/kg instead of $8.67/kg for cilantro bunches)
Ingredients with count-based usage units (e.g., "Each") no longer show incorrect validation errors or zero nutrition values after editing NFP data
Edit Ingredient modal procurement dropdown and price history dropdown now respond to mouse clicks correctly (keyboard navigation was working but mouse selection was not)
Ingredient procurements are no longer lost when saving - fixed issue where partially filled procurements could be incorrectly deleted
Vendor code field is now fully optional and correctly handles legacy numeric values from CSV imports and Sysco sync
Ingredient cost calculation now works correctly for "each" pack size units
Unit validation no longer fails due to order sensitivity when mixing "each" units with weight/volume units
Release
4.4.0
Added
Comprehensive unit test coverage for critical backend services (payments, invoices, nutrition calculations, date utilities, unit conversions) improving code reliability and catching edge cases
Fixed
Financial reports (Sales by Customer, Sales by Product, Sales by Meal, Ingredient Usage) now correctly filter orders within the specified date range, fixing an issue where orders outside the upper date bound were incorrectly included
TypeSense Admin page now correctly loads customer and user collections by excluding non-indexed fields (like image URLs) from search queries
Recipe edit modal now displays correctly on mobile devices with improved header layout, optimized table columns, and properly positioned action menus
Costing table on mobile now prioritizes Component and Amount columns by hiding Type and Cost/unit columns on smaller screens
Tab navigation on mobile now uses a 2-column grid layout instead of horizontal scrolling for better usability
Modal header buttons now have proper spacing on mobile to prevent focus states from overlapping
MC: Major stability improvements across meal plan and meeting management - fixed 18 issues including memory leaks, race conditions, silent error failures, and proper cleanup of background operations
Release
4.3.0
Release
4.2.0
Changed
Recipe importer now displays a progress ring with percentage and descriptive status messages during import, with a celebratory speed banner for imports completing in under 15 seconds
Fixed
Dashboard date range selector now displays full date text without truncation, with trailing year format for same-year ranges (e.g., "Jan 25 - Jan 26, 2026") providing clearer date context
ChefTec import now correctly handles duplicate recipe names, yield parsing with comma separators, product yield normalization, and size variant formatting
MC: Dropdown menus now work correctly in the Edit Meeting component
Release
4.1.0
Added
SendGrid Management Service for programmatic email template management, deliverability monitoring, and compliance analytics via SendGrid V3 API
Global search command palette (Cmd+K / Ctrl+K) for quick navigation to recipes, ingredients, products, customers, orders, and users with keyboard navigation support
Inline search bar in header for instant access to search functionality without opening the full modal
Real-time TypeSense search index sync for tenant-aware entities (customers, orders, ingredients, recipes, products, users) - changes sync within 2 seconds of database updates
Invoice print functionality for single or multiple invoices
Invoice export functionality for single or multiple invoices
Multi-select UI controls for invoices, to make manual selections for batch print or export
AI-powered ingredient validation that automatically filters non-ingredient items (separators, equipment, packaging, supplies) from Sysco EDI imports and ingredient cost syncs
Product lineup edit component now includes customer assignment tab for assigning/unassigning customers directly, with warnings when customers belong to other lineups
New "Sales" tab in Product edit component with customer/lineup assignment selector, includes food cost percentage column and guard when removing items with custom pricing
Fixed
Replaced all modal close buttons on the app with the shared close button component
Recipe costing now displays correct ingredient costs instead of $0.00 for ingredients with legacy procurement data
Changed
Recipe importer now displays a progress ring with percentage and descriptive status messages during import, with a celebratory speed banner for imports completing in under 15 seconds
Products, Recipes, Sub-recipes, and Ingredients list pages now load images instantly without visible pop-in
Recipe Costing tab now opens instantly instead of showing a loading delay when switching tabs
Styling updates in invoice management page
New product lineup editor with inline product selection for adding new product lineups
Customer edit component supports individual product assignment without requiring a product lineup, with per-customer price and SKU overrides
"Sold as" table removed from Costing tab in Product edit component, replaced by new Sales tab
Release
4.0.2
Fixed
Daily Prep worklist now displays correct quantities when stock tracking is disabled, matching the Print view values
Release
4.0.1
Fixed
Bulk worklist "To Make" values now save correctly when display unit differs from recipe yield unit (e.g., entering 1 kg no longer saves as 0)
Fix wrong qty/unit display in "Expiring" notifications in the Bulk workflow
Release
4.0.0
Added
External Integrations:
Public API for external integrations with API key authentication, rate limiting, and CRUD endpoints for customers, products, and orders
Webhooks system for real-time event notifications with configurable endpoints, automatic retries, and delivery tracking
Tenant Data Tools page with ChefTec JSON import for bulk importing recipes and ingredients from ChefTec exports with real-time progress logging
Public API endpoint for AI meal generator that provides complete recipe and ingredient context including costing, allergens, nutrition, and taxonomy data
Sysco integration that automatically syncs ingredient catalog and pricing via SFTP, with configurable sync modes (full import or pricing-only updates) and real-time progress tracking
Internal Infrastructure:
Backend proxy endpoint for TypeSense search when frontend cannot directly reach TypeSense server
New, layout-accurate skeleton loaders across the app (products, ingredients, workflows, orders, customers, recipe importer, invoices, vendors, and more)
Shared, modular skeleton loader component + documentation and custom command for agents for consistent implementation
Changed
Using new v2 version of the AI recipe importer API for greatly improved results quality
Many improvements to recipe importer including improved matching accuracy, improved error handling, and improved performance
Fixed
Fixed incorrect nutritional values for USDA SR Legacy and Foundation foods (values were inflated by ~3.5x due to incorrect portion-based multiplier calculation)
Re-enabled logo loader screen
Release
3.3.0
Added
Location setting to disable stock tracking on Daily Prep worklist, showing plain totals instead of Par/Stock/Needed/To Make columns (requires tenant feature flag)
Fixed
Checklist polling now correctly stops when viewing worklists in date range mode
Release
3.2.1
Fixed
MC: Meeting wizard now correctly advances to the guest invitation step instead of exiting edit mode when clicking "Next"
Release
3.2.0
Added
Batched notification system for QuickBooks Online sync errors reduces email volume by grouping errors within a 30-minute window into a single summary email
Changed
QuickBooks Online sync operations now retry up to 7 times (previously 3) with improved exponential backoff and jitter to better handle API rate limits
Fixed
Replaced all modal close buttons on the app with the shared close button component
Debug mode for superadmins that displays additional diagnostic information
Changed
Modern Catering onboarding now correctly redirects to another tenant's signup page when the user's address is serviced by a different locationain
Release
3.1.0
Added
AI-powered ingredient name matching automatically suggests USDA nutrition data when creating ingredients, with confidence-based workflows that auto-accept high-confidence matches (85%+), prompt review for medium-confidence matches (50%-85%), and allow manual search for low-confidence result
Changed
"Weight per unit" field is now required for ingredients with each-type usage units (ea, slice, portion) to ensure accurate nutrition calculations
Recipe review banners now display grouped error summaries with "Review issues" links that navigate to the relevant tab
Nutrition missing banner shown in Ingredient list and Label Data tabs with link back to Recipe tab
Warning highlight styling on recipe components when clicking "Review issues" to help identify problem items
Recipe ingredients list now displays clear messages when nutrition or cost data is missing for items
Recipes bulk editor now uses manual save instead of auto-save, with highlighting on modified rows and a floating save/discard banner
Creating a new customer now automatically creates a new default location in a single step
Default location fields are also shown in customer edit form if customer has a single location
Recipe and ingredient edit modals now maintain consistent height when switching between tabs, eliminating jarring size changes
Fixed
Nutrition panel now correctly displays "Per 1 ea" for each-type units (ea, slice, portion, piece, item) instead of incorrect "Per 100 ea"
Nutrition values now scale correctly when changing serving size for each-type ingredients
Nutrition values now properly recalculate when switching between unit types (grams ↔ each) on ingredients
Nutrition fields now clear when serving size is deleted, and restore correctly when a new value is entered
Ingredient usage report now includes protection against infinite loops from circular recipe references
Address input in customer edit component now shows all countries
Replaced all modal close buttons on the app with the shared close button component
Release
3.0.0
Added
AI-powered spec sheet importer extracts nutrition data from uploaded spec sheets, nutrition labels, and product photos
Changed
Recipe importer latency reduced from ~2.5-3 minutes to ~30-35 seconds through model optimization (gpt-4.1 for parsing) and parallel processing of validation and ingredient naming steps
Improved performance of batch inventory updates by eliminating redundant recalculation triggers
Recipe importer now performs ingredient enrichment (compliance tags, allergens) as a batched background task after confirmation, significantly reducing import times
Recipe costing errors now correctly assigned to individual recipe items instead of only the result object
Fixed
Founder follow-up email now displays as sent from Drew Munro instead of the generic support address
Ingredient usage report now includes protection against infinite loops from circular recipe references
Bulk worklist now displays correct quantities for sub-recipe components (previously showed values like "0.4 g" instead of "430 g")
Comprehensive unit test coverage for ingredient usage report generation to improve reliability
Batch inventory updates no longer crash when encountering corrupted inventory data or invalid recipe IDs
Added sugars nutrient value from USDA now correctly saves to database during nutrition data matching
TypeSense search now automatically recovers from connection errors by falling back to backend proxy, improving reliability during backend restarts
USDA ingredient search now prioritizes SR Legacy database items which have 3-5x more complete nutritional data compared to Foundation items
AI-matched ingredients now properly populate nutritional values when auto-accepted
Release
2.15.0
Fixed
Recipe import review: Converting a sub-recipe to an ingredient now properly removes the original recipe from the sidebar instead of promoting it to a main recipe
Production Sequence editor now displays correctly for tenants with meals that have no recipe category assigned
Recipe import review: Delete button now appears for stale/old imports that were previously stuck without action buttons
Recipe import review: Deleting a recipe now immediately removes it from the sidebar list and updates the pending import count
Recipe import review: Banner showing pending imports now clears properly when all imported recipes are deleted
Orders now appear on the correct production day for tenants with same-day production cutoff settings configured
Removed spurious "No dates provided" error toast when navigating between workflows with date range mode selected
Changed
Product spec sheets now download directly as PDF files instead of opening the browser's print dialog
Release
2.14.0
Added
Price history log for ingredients shows procurement cost trends over time with interactive chart and paginated list view
"Include completed items" checkbox in Print/Export dropdown allows showing checked-off items in printed worklists
Changed
"Completed by" field in workflow checklists now displays user's full name instead of username
Updated styles for Price History Log in Ingredient edit component
Fixed
Fixed mark-as-done checkbox polling so completed items appear in real-time for all kitchen staff
Bulk worklist now displays correct units and amounts for recipe components (previously showed values like "2933 kg" instead of "2.93 kg")
Release
2.13.1
Added
Ingredient to USDA food name matching report tool for analyzing how well ingredient names match USDA database entries
Nutrition source display now shows original food name from data source (superadmin only)
Changed
USDA FDC ingredient search algorithm now uses brand-aware scoring (50x boost for brand matches), generic form preference (prioritizes common variants like unsalted butter, all-purpose flour), and processing form logic (prioritizes core ingredient over form), improving search result relevance from 63% to 100% pass rate on internal test suite
Fixed
Production workflow now includes shared (system-level) recipes and ingredients alongside tenant-specific data
Small ingredient amounts in grams and milliliters (< 10) now display with 1 decimal place instead of rounding to 0 (e.g., 0.4g now shows as "0.4 g" instead of "0 g")
Fixed deduplication script for usage and stock entries to work with all databases
Release
2.13.0
Added
Configurable parameters for cron jobs with dynamic form fields in the admin UI (starting with "Days Back" option for Regenerate Production Days)
Changed
Ingredient usage report now loads only products, recipes, and ingredients referenced in the selected date range orders, significantly improving performance for large databases
Fixed
Product sales report progress updates now use tenant-scoped options to avoid hanging progress when running in background worker contexts
App now automatically recovers from white screen after deployments by detecting stale chunk load errors and reloading
Release
2.12.0
Added
Report cancellation endpoint (POST /reports/tasks/:id/cancel) with tenant-scoped ownership validation and cancellation UI hooks
Unit coverage for report task cancellation authorization, including tenant isolation checks
Changed
Financial and product report generators now use a shared cancellation check interval and halt processing as soon as tasks are cancelled
Report finalization now propagates tenant options through callbacks to avoid cross-tenant updates and duplicate task transitions
Removed redundant cancellation checks during financial report assembly for clearer background processing control flow
Documented cancellation polling rationale and helper behaviour for report task lifecycle handling
Report cancellation now enforces tenant-matching for ManageOwnTenants users even when task metadata lacks tenant IDs, reducing cross-tenant risk in admin flows
Report cancellation endpoint authorization metadata now allows tenant managers alongside report viewers while retaining owner/admin runtime checks
Reports no longer auto-generate on page load; first date range selection triggers generation, subsequent changes require clicking Apply
Fixed
Excel exports for Products and Sub-Recipes no longer get stuck loading when exporting large datasets
Side panel and backdrop are now hidden in printed workflow views
Date range selection exceeding maximum allowed days now shows a toast notification and clears the selection instead of displaying an inline error
Fixed duplicate stock entries appearing in stock history modal (multiple "Used in products", "Produced", "Expired" entries for the same date). Added duplicate detection, cleanup logic, and unique database constraints to prevent future duplicates from race conditions during concurrent production day recalculations
Release
2.11.1
Fixed
Recipe methods (preparation instructions) are now included when printing worklists in date range mode with "Include methods" enabled
Fixed print view showing 0 amounts for top-level component rows in Daily Prep worklist when using "Group by components" mode
Release
2.11.0
Changed
Added production setup wizard button to location settings
Clicking save on company location settings now doesn't navigate away
"Send test email" on production settings (in company location settings and production setup wizard) now uses the real time
Multi-tenant repository delegation now correctly propagates tenant context across different repositories, ensuring all database operations use the correct tenant datasource
Fixed
Daily Prep worklist now correctly shows quantities for sub-recipes that are also used in Bulk recipes, preventing double-counting that inflated required amounts
"Used In" breakdown on Daily Prep worklist now only shows recipes that directly use the item, excluding indirect usage through Bulk recipes
Fixed issue in order reminder email where an incorrect cutoff date would be shown whenever the reminder is sent more than one day before
Fixed dropdown width issue (again) in tenant and sub-tenant switchers in main header
Fixed products from assigned product lineups not appearing when viewing customer details
Release
2.10.0
Added
Welcome dashboard with personalized onboarding checklist that shows relevant steps based on business type, and automatic onboarding completion tracking that hides the welcome dashboard once key milestones are reached (first test order or internal production order)
Changed
Customer type selection replaced with a single checkbox for "Internal customer". Additional customer fields are hidden when this option is selected.
If a single product lineup exists, it's assigned to new customers by default. Inversely, when creating the first product lineup, it's auto added to any existing customers that didn't have a lineup.
Release
2.9.0
Added
Date range selection mode for kitchen production worklists, allowing aggregated view across multiple production days for planning and forecasting
Changed
Optimized new order save performance by making email sending non-blocking and parallelizing validation queries
Optimized kitchen production worklist queries by batching production day record operations
Fixed
Added date range validation for kitchen production list URL params (auto-swap if start > end)
Release
2.8.1
Fixed
Bulk worklist orders modal now correctly shows only current period orders (orders for execution days after the bulk production day)
Release
2.8.0
Fixed
Multi-tenant database operations now correctly route to tenant-specific databases during background processing (e.g., recalculation queue workers)
Changed
Refresh button on workflows now always re-calculates data for present and future production days. For past days it still re-fetches pre-generated data.
Release
2.7.0
Changed
Improved styles for product, recipe and ingredient bulk editors
Removed unused fields in bulk editors
Updated ingredient procurement fields in bulk editor to match current structure and naming
Improved global styles for pagination controls
Fixed
Fixed checkbox layout issues across the app.
Release
2.6.0
Changed
Simplified nutrition facts panel displays by removing interactive serving size controls (serving size now controlled by parent components only)
Recipe component swapping is now instant with costing and nutrition data loading in the background
Release
2.5.1
Fixed
Fixed orders missing from production day view after order modifications when recalculation runs outside HTTP request context (e.g., from queue workers)
Release
2.5.0
Changed
Production day real-time recalculation now uses a two-phase approach: broadly invalidates future days for on-demand refresh while immediately recalculating only critical affected days within a 2-week window
Fixed
Cutoff time calculation was sometimes assuming production day was the day before order delivery regardless of production settings
Order, Recipe, Ingredient, Products, etc. updates no longer trigger duplicate production day recalculation events
Release
2.4.3
Fixed
Fix an issue with data serialization that caused QBO tokens to be assigned to an invalid tenantID, which caused QBO connections to fail after first refresh attempt.
Release
2.4.2
Fixed
Printing workflows grouped by Customer or Component wasn't working, printed an empty page
Release
2.4.1
Added
Verified Library icon indicator for library items in recipe and ingredient tables
Fixed
Removed unnecessary loading spinners when opening recipe/ingredient modals in workflow pages
Checkbox and form styling inconsistencies across customer management and production settings
Release
2.4.0
Added
Optimistic Save: Edit modals now close instantly when clicking Save, without waiting for the server to respond. The save completes in the background, letting the user continue working immediately.
Changed
Recipe and ingredient lists now preserve their state when opening or closing edit modals instead of refreshing and losing scroll position, expanded state, etc.
Editing a Verified Library item now seamlessly creates your own copy and updates the list without requiring a page refresh
Fixed
Fixed checkbox and form styling inconsistencies across customer management and production settings
Fixed "unsaved changes" modal incorrectly appearing when opening a recipe in edit mode and exiting without making changes
Opening and closing recipe/ingredient modals in Workflows pages doesn't reload the data anymore
Release
2.3.1
Fixed
Fixed centered modals getting cut off when content exceeds screen height
QuickBooks Online token refresh no longer loses connection when multiple requests refresh simultaneously
Release
2.3.0
Added
Redo support for edit screens (Ctrl+Y or Cmd+Shift+Z to redo undone changes)
Option to remember expanded/collapsed row states in worklists across browser sessions (toggle in workflow options panel)
New category editor with sidebar navigation for switching between recipe, ingredient, and tag categories
Searchable category tables with inline editing, add, and delete capabilities
Changed
Worklist now checks for newer data before refreshing when returning from inactivity, reducing unnecessary loading
Added exception to prevent app update dialog to show in unauthenticated pages
Updated favicon to Demi icon!
Updated login pages (login, forgot password, password creation, multi-factor-authentication) to display a Modern Catering version if logging in from the /mc route or from an activation link sent from Modern Catering
MC branding persists across login page navigations within a browser session, even if the URL parameter is removed
Every Modern Catering email that includes a link will now include a parameter so that the app knows to redirect to Modern Catering (for example, if the user is logged out, the link will lead them to MC-branded login)
Fixed
Prevent inheriting ingredient procurements (don't copy parent company's pricing into subtenants when versioning ingredients)
Stock history modal now displays amounts in the correct unit (e.g., kg instead of showing grams with kg symbol)
Fixed workflows column width bug where item name column would get shrunk down too much
Users signing up for Modern Catering will now be taken to the MC dashboard to complete their setup instead of the Demi team member setup.
Fixed customer logo image sizing issue on MC top bar
Fixed styles on internal MC customer switcher
Release
2.2.1
Fixed
Needed vs To Make calculations for the Daily Prep workflow were not showing accurate values
Release
2.2.0
Fixed
Bulk editor now loads data correctly for Products, Sub-Recipes, and Ingredients lists
Location settings can now be saved without "payload too large" errors caused by duplicate production day entries
Release
2.1.0
Added
Collapsible "Older entries" section in stock history modal to view historical stock movements
Fixed
Stock values now match between worklist row and stock history modal
Production day regeneration no longer gets stuck when sub-recipes have been deleted
Soft-deleted recipes are now included in worklist generation to maintain order integrity
Progress tasks stuck for more than 10 minutes are automatically marked as failed
Fixed: Workflows now correctly load all ingredients during automatic regeneration (triggered by order changes). Previously, only sub-recipes appeared while ingredients were missing.
Release
2.0.1
Fixed
Fixed issue where QBO status was not being updated correctly when the user was redirected to the QBO authentication page
Release
2.0.0
Added
Bulk Inventory System for sub-recipe level stock tracking with shelf-life awareness
FEFO (First-Expire, First-Out) stock consumption automatically uses earliest-expiring batches first
Automatic inventory recalculation when orders or recipes change
Expired stock tracking with batch-level visibility
Window-based on-hand calculations for Bulk production periods
Progress tracking for long-running inventory recalculations
Changed
Complete redesign of the Workflows UI, with significant performance improvements, new features, and a new look
Recipe expansion now uses BFS algorithm with O(S×L) complexity, reducing CPU usage by 40-60%
Stock and usage entries now track at sub-recipe granularity rather than product level
Production day dates respect customer same-day delivery settings
---
Release
1.5.0
Added
Added PostHog integration
Added "Last modified" date to recipes in detail view, printable recipe and printable spec sheet
Changed
Product spec sheet now always displays the dietary notes section for consistency
Browser tab shows product/sub-recipe/ingredient name
Recipe and spec sheet print modes set the item type, name and ID as the filename when saving to PDF
Removed legacy unused SendGrid template IDs
Release
1.4.1
Added
Restored Modern Catering cart toggle functionality
Changed
Changed font across all Modern Catering UI to the standard Demi font
Release
1.4.0
Changed
Switch to new Demi stripe account
Release
1.3.9
Changed
Revamped image display component so that images throughout the app now consistently resize responsively to fit their containers, preventing overflow on smaller screens while maintaining aspect ratio
Fixed
Don't show "Print spec sheet" link on sub-recipes
Release
1.3.8
Added
Printable product spec sheet accessible from the recipe edit page
Changed
Redesigned printable recipe view with improved layout and styling
Release
1.3.7
Fixed
Fixed image sizing for image display component and order edit
Release
1.3.6
Fixed
Fixed bug in Companies search input, now working properly again. Added (our first!) integration test
Changed sender for Modern Catering emails to UpMeals support instead of Demi
Style fixes and improvements for Modern Catering: Company selector (when switching to MC), onboarding modals, Meal Plan management, Meal Plan detail and edit, Meal Plan checkout
Fixed dropdowns getting cut off in team member and meal plan edit components
Fixed MC Meal Plan management bug where the component would refresh as soon as an edit modal was opened, closing the modal in the process
Changed
Added role filter so only MC roles show when adding/editing a team member from the MC UI
General style updates across the MC UI
Switched outdated text inputs with regex validation for email input to HTML email-specific inputs which handle validation natively
Added
Toast notifications to confirm making changes to Meal Plans or adding/removing participants
Release
1.3.5
Fixed
Fixed server CORS configuration issues
Tighter security for admin user credentials
Release
1.3.4
Fixed
Modern Catering: Fixed header bugs layout and style bugs, and other style adjustments across the MC UI
Mordern Catering: Fixed ingredient lists not displaying in product details modal and hid "Extra nutrients" link from NFP
Modern Catering: Don't show announcement banner
Fixed bugs in printable recipe view, it was showing workflow data and hardcoded UpMeals logo
Changed
Modern Catering: Fine-tuned cart breakpoints
Modern Catering: Notifications component in MC header only show relevant MC notifications
Release
1.3.3
Fixed
Fixed display bug in printable workflows which wasn't adding spacing between columns
Release
1.3.2
Added
Nutrition data now shows its source (USDA FDC, Nutritionix, or Demi AI) when viewing ingredients and nutrition facts panels
Brand name from nutrition data sources is automatically populated when importing ingredient nutrition data
Changed
Image uploaders now have drag & drop functionality
Image uploaders have tailored layouts for user profile pictures and company logos, with clearer previews for each
Logo and profile image uploads now surface a reminder message when there are unsaved image changes
Improved ingredient nutrition data search with better layout and dropdown positioning
Fixed
Fixed an issue where uploaded logos occasionally did not show the latest version because of browser caching
Release
1.3.1
Changed
Changed help link to getdemi.co/help instead of ZenDesk
Fixed
Cholesterol DV% now shows in exported PDF NFPs
Fixed error preventing saving changes to customers
Release
1.3.0
Added
Replaced UpMeals logo for dynamic company logo in customer ordering portal and printable kitchen workflow lists and packing slips
Internal customer creation during signup flow for companies with internal business categories
Changed
Standardized business category options by company type (food operator vs non-food operator) on onboarding and company settings
-
Fixed
Fixed order reminder test email functionality in company location settings
Release
1.2.3
Fixed
Fixed Modern Catering layout bug
Release
1.2.2
Fixed
Fixed status drodpown selector in product edit component
Release
1.2.1
Fixed
Fixed issue where SaaS Admin roles were not able to see the "Customers" dropdown in the "Users" management screen
Release
1.2.0
Added
USDA verified nutrition database with 400,000+ foods and improved search accuracy through intelligent ranking and synonym matching
Changed
Centralized e2e test credentials management for improved test maintainability
Enhanced testing strategy documentation to emphasize template modification as first solution for selector issues
Fixed recipe importer E2E test selector to avoid strict mode violations by scoping to desktop table
Fixed
Fixed recipe importer E2E test failing due to strict mode violation from duplicate mobile/desktop file name selectors
Release
1.1.0
Added
Stripe price selection helpers and unit tests.
Changed
Pricing now uses newest active recurring Stripe price by currency/interval; annual shown as monthly equivalent; hide currency toggle if only one currency exists; labels and subscription use the price currency.
Improved ingredient importer completion screen styling and visual feedback
Enhanced Nutritionix API error handling with clearer error messages and better diagnostics for API issues
Fixed
Fix an issue where users would be forecully logged out in some scenarios when receiving 401 errors from the API
System-wide announcement banner that displays at the top of the application for both logged-in and logged-out users
Announcement management in Settings page allowing administrators to create, edit, and schedule announcements with start and end dates
Dismissible announcements that remember dismissal state across sessions
Changed
Rebranded application from UpMeals to Demi with updated logo, color scheme, and branding throughout the interface
Guided tours now automatically save "dismissed" status when closed by any method (X button, ESC key, backdrop click, or completion), ensuring tours won't re-appear once dismissed
Updated toast notification design, now using more modern, simpler "snackbar"-style notifications
Updated toast message copy throughout the app
Removed specific undo delete toast component; simplified logic to handle with the general toast component
Release
UpMeals_8.10.0
Fixed
Fixed pack size validation error when adding new ingredients that incorrectly showed missing information even when the field was filled
Added
Dynamic company logo now displays on ordering UI, printable packing slips and printable kitchen workflow lists
Changed
Updated backend libraries to the latest stable versions, with performance and security improvements
Improved validation UX when trying to remove all tenants from a user
Fixed
Fixed permission assignment issue so that customer selection dropdown no longer incorrectly appears when editing Admin users
Release
UpMeals_8.9.0
Added
Customer creation wizard guides new users through a two-step process to create customers with at least one location
Fixed
UPC generation logic now correctly handles Company Prefix precedence: sub-tenant > tenant > global
Changed
Allow UPC prefixes to be 6 to 10 digits long (previously only 6 digits were allowed)
Release
UpMeals_8.8.0
Added
Backend logic for handling datasource cleanup when app stops
Changed
Added filter to header tenant switcher to only show tenants that completed onboarding
Enabled change detection in location settings to disable saving when no changes have been made
Only show global loader for authenticated users in authenticated routes
Fixed
Fixed modal dialog issue where clicking and dragging text inside a modal then releasing outside would unintentionally close the modal
Fixed a bug where ingredient allergens were not being fetched and shown in the edit ingredient screen
Release
UpMeals_8.7.1
Added
Added fuzzy search capability in recipe component list dropdowns for adding and swapping components, greatly improving search functionality
Added new columns to Companies management: Owner name and email, and "Created on" date
Fixed
Companies page status filters now match displayed subscription statuses and correctly separate active trials from expired trials
Fixed status and tier badge display, and improved styling
Release
UpMeals_8.7.0
Added
Database backup and reset automation scripts for local development and CI/CD environments
Automated database backup script for creating snapshots of MySQL and MongoDB databases
Comprehensive E2E test suite for Recipe Importer covering full import workflow, recipe verification, and automatic cleanup
Recipe Importer test infrastructure including page objects, fixtures, and test plan documentation
Changed
Address and phone inputs now display all countries, with Canada and US at the top
Timezone selectors now display UTC offset alongside timezone name (e.g., "(UTC-08:00) America/Vancouver") and are sorted byu UTC offset.
Address and phone inputs now show required indicator when validation is enabled
Recipe Importer component templates updated with data-testid attributes for improved testability
Prevent new orders until at least one customer has an active B2B location, with in-app guidance to add a customer
Improved layout and style in recipe component list dropdowns for adding and swapping components, with much clearer distinction for sub-recipes and ingredients
Fixed
Fixed required handling and indicators in adress input
Fixed visibility bug where fields were hidden when editing orders
Fixed order management filter selectors that were breaking e2e tests
Fixed error preventing user edit modal from rendering properly when assigning multiple tenants to a user
Fixed BULK worklist "Needed" column always displaying 0 instead of showing actual production quantities required for upcoming orders
Release
UpMeals_8.6.2
Added
QBO ID fields added to customers, customer locations and products list pages - only visible to super admin users
Changed
Revamped QBO Status Service to provide more detailed status information and error messages
Return Realm ID from QBO Status endpoint to be used in QBO integration
Release
UpMeals_8.6.1
Fixed
Fixed critical issue where non-super admin users could lose access to their data when sub-tenant (location) context was not properly maintained during login or tenant switching
Added automatic sub-tenant selection for non-super admin users to ensure they always have a location context when accessing their data
Implemented intelligent error recovery that automatically selects a sub-tenant and retries requests when backend detects missing location context
Enhanced validation to prevent invalid sub-tenant states across frontend and backend
Added 42 comprehensive unit tests covering sub-tenant auto-selection, error recovery, and validation logic
Release
UpMeals_8.6.0
Added
Production Setup Wizard guides new users through configuring their kitchen workflow in 5 steps: production days, order cutoffs, email reminders, bulk prep days, and confirmation
New production schedule settings UI in location settings with improved controls for managing production days, order cutoff times, and email reminders
Support for configuring order cutoffs per production day or as a single weekly cutoff across all production days
Added "Send test email" functionality for order reminder emails, to be sent from the production setup wizard and production settings to preview email formatting before they are sent to customers
Changed
Simplified order cutoff day selection with preset options (Same day, Day before, 3 days before) instead of manual number input
Release
UpMeals_8.5.3
Fixed
Fixed issue where file uploads were not working correctly
Release
UpMeals_8.5.2
Changed
Toggle switch component revamp with updated styles, performance and accesibility
Customer edit page reorganized with improved visual hierarchy: swapped Address and Contact info block order
Customer locations page styling refreshed
Order creation screen layout improved when both customer and location fields are present
Fixed
Restored same-day delivery options in customer edit component
Release
UpMeals_8.5.1
Fixed
Added logic to clean up invalid permissions from all roles in the system
Release
UpMeals_8.5.0
Added
Tenant and sub-tenant switchers added to Modern Catering and customer topbars for consistent multi-tenant navigation
Created Component Specification Guide for AI agents to create and maintain component documentation with standardized structure and freshness tracking
Changed
Roles are now filtered by user permissions to avoid exposing sensitive information to users with lower permissions
Roles are now filtered for screen-specific operations (B2B, MC, SAAS)
Extra security for user management endpoints - prevent users with lower permissions to create or edit users with higher roles
Tenant and sub-tenant switcher components converted to standalone Angular components for better reusability
Reorganized AI agent documentation into consolidated topic-based standards files for improved discoverability and maintainability
Implemented freshness tracking metadata system for core documentation to monitor documentation health and maintenance schedules
Edit order component UI improvements: status badges, layout adjustments, customer dropdown behavior, filter bar styling, search input, order totals section
Orders management page UI improvements: action buttons (Packing slips, Sync orders from QuickBooks) are now organized into an Actions dropdown menu, general
style adjustments in header, filter bar and main table
"Sync orders from QuickBooks" option now depends on the tenant's showQboOptions configuration setting
Removed all "Generate SV orders" functionality
Fixed
Logo loader now disappears as soon as the page is loaded, instead of the fixed 3-second delay
Fixed change detection issues where closing order confirmation modals was triggering the
confirmation dialog
Fixed Reordering delivery date issue by making it so that reordering doesn't populate the
delivery date
Release
UpMeals_8.4.1
Fixed
Removed Organizing, Production and Packing from Workflow options in sub-recipe management filters and sub-recipe edit form
Release
UpMeals_8.4.0
Added
Follow-up onboarding email sent to new signups on a cron job
Established Playwright e2e testing foundation with reliable authentication and 9 passing baseline tests
Changed
Migrated e2e tests from Cypress to Playwright with improved test stability and faster execution
Fixed
Fixed a bug affecting frontend unit tests
Release
UpMeals_8.3.1
Fixed
Fixed issue where recipe costing was empty after recent changes to the database schema
Release
UpMeals_8.3.0
Added
Integrate with TypeSense for fast full-text "fuzzy" search of recipes and ingredients
Automatically reindex the TypeSense collections when recipes, ingredients, and their dependencies are created, updated, or deleted
Add reindexing cron job to automatically reindex the TypeSense collections nightly
Fixed
Workflow names in Recipe edit screen now display correct updated names
Changed
Kitchen Worklist tag category is now protected from editing or deletion to preserve system integrity
New notification toast for placed Orders
Eliminate duplicate customer name in order screen when the customer location name contains the customer name
Release
UpMeals_8.2.2
Changed
Fixed order cancellation for B2B orders without Stripe payments - orders now cancel successfully without displaying payment configuration errors
Order cancellation now prevents inconsistent states when Stripe is disabled after payment, providing clear error messages and blocking cancellation until refunds can be processed
Fixed logic that ensures that products are always created for active meals
Created a script that creates missing products for active meals
Imported ingredients are now always created as active
Release
UpMeals_8.2.1
Added
Notifications now display color-coded category badges (Recipe, Meal plans, Ingredient importer, Recipe importer, System) making it easier to identify notification types at a glance
Changed
Improved notification styles: header icon and dropdown
Release
UpMeals_8.2.0
Fixed
Style fixes: workflow buttons, users table
"Sync to QBO" button in invoice edit component now properly shows loading state instead of disappearing
Fixed sizing and position for close button in MC shop Product details modal
Fixed address input styles in MC join flow
Changed
Added status column to users table
Super Admin users are now only visible in the Global Admin scope, keeping tenant-specific user lists clean and showing only users assigned to that tenant
Roles are conditionally shown on edit user screen based on the user's role and the tenant configuration
' Admins can now see the "Users" link in the sidebar under Sales > Users
Release
UpMeals_8.1.0
Changed
User activation and order notification emails now use multi-tenant white-label templates branded with each tenant's company name and logo, replacing the previous UpMeals-branded B2B templates.
User activation email logic updated to work with the multi-tenant and multi-level user hierarchy structure (tenant users, customer users, and MC customers).
Fixed
Fixed issue where team members invited to existing companies were incorrectly shown the onboarding flow, which could overwrite company data
Release
UpMeals_8.0.3
Fixed
Sub-Tenant wasn't always being enforced for some users, leading to wrong data in their Products list
Release
UpMeals_8.0.2
Changed
Added a 2.5-second delay on the announcement modal after entering the dashboard
Style adjustments for announcement modal
Fixed
Removed fixed 3-second loader on home component - Now loads in < 1 second
Release
UpMeals_8.0.1
Fixed
Fixed Meals<>Products migration script to account for multiple products pointing to same meal
Release
UpMeals_8.0.0
Added
Unified modal and guided tour system for feature announcements and onboarding, providing consistent experiences when introducing new features or guiding users through workflows.
Changed
Products and Meals are now the same concept, using the name "Product"
Changes to onboarding signup flow with updated questions and tenant config data
Updated user-facing messages to use "product" terminology consistently instead of "meal" across the app
Renamed "Products" menu item back to "Recipes" in the sidebar navigation
Product conversion to sub-recipe is now blocked when the product appears in active orders, preventing order fulfillment issues
Fixed
Fixed incorrect warning "This sub-recipe is being used in at least one other recipe" when converting a product to sub-recipe and back to product
Release
UpMeals_7.25.2
Changed
Intercept errors when app is outdated and show the "New version available" dialog
Release
UpMeals_7.25.1
Fixed
Fixed issue where the sub-tenant context was not automatically setting the default available sub-tenant
Release
UpMeals_7.25.0
Added
Implement QBO integration widget in Settings > Integrations
Changed
Change how QBO tokens are looked up and stored in the database to be tenant-specific
Release
UpMeals_7.24.4
Fixed
Fixed issue where ingredients were not being fetched correctly in kitchen production
Release
UpMeals_7.24.3
Fixed
Quantity parsing in Recipe Importer when the raw output is a string containing fractions was sometimes failing to convert to decimal numbers
Fixed issue where PAR and stock updates were not invalidating the production day records cache
Release
UpMeals_7.24.2
Added
Interactive guided tours for key features, starting with recipe editing, to help new users learn the interface more effectively
Fixed
Category options not updating when user changes recipe type from meal to recipe
Special unicode characters for fractions were ignored from recipe methods in recipe importer
Fixed breadcrumb styles in recipe/ingredient edit components
Fixed price and SKU input field layout bug in product lineup tables
Improved overall styles in pricing lineup tables
Fixed badly positioned error message for unit mismatch in edit ingredient component
Fixed "Needs review" badge alignment in Recipe importer review
Release
UpMeals_7.24.1
Added
Included a setting in Customer Locations to enable/disable holiday deliveries
Changed
Converted AI sub-agents to research-focused approach
Fixed
Wrong dates shown in datepicker and printed worklists in Workflows page
Release
UpMeals_7.24.0
Added
Added UI controls to set ingredient 'status' (active, inactive, draft)
Added filters and a column for 'status' in the ingredient listing page
Added status propagation logic for recipes and ingredients
Changed
Remove the "confirm" button in the RI review ingredient details screen
Fixed
Fixed Playwright issues in GitHub workflows. End-to-end tests now run reliably after merges to main.
Some ingredients in the review screen were marked as needing review for no apparent reason
Release
UpMeals_7.24.0
Added
Instantly save changes to recipes and ingredients in the recipe importer review screen
Changed
If the user imports a recipe that already exists, the importer will now create a duplicate of the recipe instead of skipping it
Release
UpMeals_7.23.1
Changed
Move background processing of created ingredients/recipes to the backend
Release
UpMeals_7.23.0
Fixed
Fixed app startup script errors when there was conflicting tenant data
Fixed console errors and crashes in recipe component lists when data was inconsistent
UI fixes for recipe importer review screen
Release
UpMeals_7.22.0
Changed
Refactored recipe repository for better performance and code organization
Code updates to work with latest version of recipe importer pipeline
Fixed
Refactored default translatable repositories to solve some issues when saving translations
Fixed layout issues in edit product lineup component and made header field and button sticky for improved user experience
Release
UpMeals_7.21.2
Fixed
Fixed issue where recipe importer was not properly setting the tenant ID in the API requests
Fixed issue where the raw recipe importer output was not being saved to the database
Release
UpMeals_7.21.1
Changed
UI/UX improvements to Product Lineup edit screen: Replaced bottom dropdown with top-positioned "Add product" button and added keyboard editing capability to improve the product lineup creation process
Release
UpMeals_7.21.0
Added
Action to "mark as reviewed" in ingredient and recipe edit screens
Added URL-based navigation for recipe importer review screen
Shared close button component (um-close-button) for consistent modal UX across the app with standardized styling and accessibility
Changed
Recipe name editing now saves instantly with visual confirmation
Removed "More info" button from component review
Missing amount warnings now appear directly in the amount field for clarity
Recipe importer review component swap dropdown now opens at the currently selected item instead of the top of the list
Hide from listing screens the ingredients and recipes that haven't been confirmed in the Recipe Importer
General recipe importer review UI/UX improvements: layout, styling, copy
Improved breadcrumb styling for clearer emphasis
Hide recipe import review sidebar when importing a single recipe
Updated recipe importer review ingredient component styling to match modern design patterns with improved consistency and visual hierarchy
Fixed
Recipe importer breadcrumb now updates immediately when recipe names are saved
Improved responsive layout and table organization in recipe importer review
Fixed recipe importer showing "No data for review" when refreshing the review page.
Fix incorrectly flagging an ingredient as a poor match when it lacks usage amount/unit
Fixed ingredients flagged as "needs review" with no apparent reason
Fixed the 'needs review' badges in recipe/ingredient listing and details screens
Fixed components being tagged as "matched" when they were actually "new" in recipe importer review
Release
UpMeals_7.20.0
Added
Added ability to convert between ingredient <-> sub-recipe <-> meal
Added multilingual support with automatic language detection based on location
Added language switcher to recipe editing for viewing translations when French is enabled
Changed
Standardized confirmation dialogs across all recipe and ingredient conversions, making the experience more consistent and predictable
Conversion success messages now persist after navigation to the newly converted entity, ensuring users receive clear feedback
Improved ingredient importer service to better handle allergens, translations, and categories
Language controls now conditionally display based on tenant configuration
Fixed
Fixed missing confirmation dialogs for some conversion actions that previously converted without asking for confirmation
Fixed unit tests and CI/CD integration tests
Fixed product name validation to prevent duplicate names
Fix long loading time when converting sub-recipe to meal
Release
UpMeals_7.19.2
Fixed
Fixed nutrition facts display showing undefined values instead of zero, ensuring consistent PDF rendering
Fixed recipe importer having two confirmation dialogs when deleting last recipe
Fixed importer getting frozen on "loading data for review"
Properly clear cache when product is created, updated, or deleted
Release
UpMeals_7.19.1
Fixed
Fixed ingredient label compliance sync cron job
Isolate tenant database scope
Implement forceSync parameter
Release
UpMeals_7.19.0
Added
Prevent ingredients from being auto-tagged with label compliance tags multiple times
Added
Added new action menu to edit recipe modals, and moved Print action there; can be
copied to any component that has additional actions
Added delete recipe action to edit recipe component
Changed
Recipe save button label now includes context about recipe status changes
Release
UpMeals_7.18.3
Fixed
Fixed translations not being saved for recipes
Release
UpMeals_7.18.2
Added
Smart loading screen with CSS animation that persists through tenant loading and route resolution
Fixed
Fixed ingredient list sorting, where some sub-recipes with usage units of "each" were not being sorted correctly
Fixed endless spinner when saving product lineups
Release
UpMeals_7.18.1
Added
Added status toggle controls to action menus in meal list
Fixed
Fixed bug causing status changes not to be saved in recipes
Added missing "inactive" badge to recipe detail view
Changed
Removed "Edited" badge from recipes
Removed tooltip from dropdown filter component
Release
UpMeals_7.18.0
Added
UPC uniqueness warning in Recipe editor: when a manually entered UPC is already used, the app shows a confirmation dialog listing the existing product(s) and lets the user proceed if desired.
Changed
UPC validation changes
Client-side UPC validation now requires exactly 11 numeric digits; empty UPC is treated as null (backend may auto-generate for Meals).
Uniqueness check is skipped when the UPC hasn’t changed from the saved value.
Conflict results are filtered to exclude related versions (parent/child/sibling) to avoid false positives.
Standardized and simplified edit component modal footers across the app and removed unnecessary buttons, making editing anything in the app simpler and more intuitive
Prompt the user to invalidate parent recipes when a sub-recipe that's being used in other recipes is set to inactive
Release
UpMeals_7.17.1
Changed
Remove the "verified" property from recipes, replaced it with new status system:
Active: The recipe is active and can be used in meal lineups
Inactive: The recipe is inactive, it cannot be used in meal lineups
Draft: The recipe is a draft, it cannot be used in meal lineups or assigned to products
Fixed
Fixed a bug that allowed UPCs to be duplicated across tenants
Release
UpMeals_7.17.0
Fixed
Generating new UPCs was not working properly
Fixed a bug that allowed UPCs to be duplicated across tenants
Changed
Updated dialog alert styles across the app to new much slicker design.
Release
UpMeals_7.16.8
Fixed
Fixed Stripe integration not dynamically loading tenant's publishable key
Release
UpMeals_7.16.7
Fixed
Prevent scroll wheel from changing number input values throughout the app
Release
UpMeals_7.16.6
Added
Editing a recipe shows the auto-calculated total weight in the "Total weight" field as a placeholder
Added cards for Sysco and QuickBooks online to Settings > Integrations with a "Coming soon" message
Fixed
Fixed email not saving in customer and customer location edit
Clearing import data didn't work when entering review mode from a notification instead of waiting for the import to finish
Fixed issues with NFPs in sub-recipe edit screen
Release
UpMeals_7.16.5
Changed
Product lineups style revamp: improved visual style in product lineup management and edit components
Renamed "QBO Prefix" field to "Lineup ID"
Fixed
Sentry: Add beforeSend filter to drop 4xx events globally (keeps logs/breadcrumbs but no issues); only 5xx/network are captured.
Fixed cache issues when saving a customer/product
Release
UpMeals_7.16.4
Fixed
Auth/API: Treat any HTTP 401 as an expired session and redirect to login (instead of relying on specific error messages). Prevents noisy Sentry errors like UPMEALS-OS-8 on /mc/dashboard when /me returns 401.
Properlly handle errors in /me endpoint
Add functionality to log out of other devices in user profile page
Release
UpMeals_7.16.3
Fixed
Fixed issue where swapping a recipe component would not update the amount/unit input
Release
UpMeals_7.16.2
Changed
Added tenant switcher for Customer Admins that also have access to other tenants
Restricted Promo codes main nav link to tenants with MC access only
Removed "default location" from the name of default locations
Release
UpMeals_7.16.1
Changed
Ingredient editor: replaced Quantity and Pack size inputs with dedicated amount-unit input component
Improved reliability of amount/unit updates to avoid flicker or unexpected changes
Removed 1:1 conversion ratio error banner
Added button to dismiss alert banners
Allow deleting all procurements from ingredients
Fixed
Reports: Add null‑safe guards around filter dropdown lookups to prevent .find on undefined during async initialization.
Amount/unit selector:
refactored to unidirectional data flow with local state to avoid change detection loops
fixed unit change not converting amounts (eg. 1500 g → 1.5 kg)
Allow decimals in amount input
Fix precision issues when converting from oz -> g
Dismiss recipe error banners when issues are resolved
UX fixes to async data fetching when swapping a recipe component
Release
UpMeals_7.16.0
Added
Recipe editor: Swap components directly in the components list (edit mode)
Selecting a replacement preserves position, amount, and unit where possible
Supports cross-type unit conversion (each/weight/volume) using nutrition and recipe ratios
Make Shelf Life not required in the recipe edit screen
Release
UpMeals_7.15.0
Fixed
Further improvements to the Recipe Importer error handling logic
Fixed Ingredient Importer not clearing the URL query, so it reappeared after page refresh
Removed static "Inactive" and "Edited" badges from ingredient edit component
Fixed "New" badge style in ingredients management
Release
UpMeals_7.14.0
Changed
Changes to GitHub Actions workflows
Pushing to develop triggers CI to dev server and e2e tests
Removed stage branch triggers
Pushing to main triggers CI to staging server
Production deployment is now triggered manually
Updated customer location type dropdown options
Fixed
Added error handling for recipe export in Recipe Management screen
Fixed an issue where a typed amount could disappear after selecting a unit
Fixed a rare rounding display glitch where whole numbers could show long decimals
Fixed issue with compliance tag sync script where wrong tags were not being cleared from ingredients
Fixed errors on onboarding when there are naming clashes for default location
Release
UpMeals_7.13.0
Changed
Users are now only able to see their own pending imports
Changed options for finalizing a recipe import: Finish, Start Over, and X (Cancel)
Updated currency detection logic to use the new detectCompanyCurrency function
Fixed
Fixed: When clicking on a "review imported recipes" notification, the user was presented with a blank page when there were no pending recipes to review
Switched label generator endpoint to latest one, fixed code to send valid payload
Fixed label compliance tagger logic
Fixed issue where tenant slug was not being generated correctly and causing errors in onboarding flow
Removed debug code that was bypassing company deletion confirmation (for super admins)
Release
UpMeals_7.12.0
Added
Added new fields to Ingredient model for importer: base, varietal, form, storedFormat
Implement new logic for creating required columns for similarity search
Fixed
Fixed compliance tag not being saved for ingredients
Fixed validation errors in Company component
Fixed scrolling issues in Recipe Importer
Fixed wrong order types in Orders management
Fixed dropdown panel width issues
Fixed Recipe Importer review screen not re-opening when refreshing the page
Fixed the "restore original match" button appearing for ingredients that haven't been swapped in recipe importer review screen
Release
UpMeals_7.11.1
Fixed
Fixed display issue for order status badges in Orders Management page
Refactor image name generation across all edit components for consistency and avoiding potential clashes
Release
UpMeals_7.11.0
Added
Added custom logic to exclude water used-and-drained from ingredient list for recipes
Changed
Deleting a recipe in the recipe import review screen now also removes its components
Revamp recipe importer progress screen
Fixed
Fixed conversion between volume and weight units for auto weight calculations
Fixed cache invalidations for customer lineups and cached product data when ingredients or nutrition data were changed
Send MC meeting cancelation emails only for 'live' meetings
Address autocomplete UI fixes
Fixed search functionality in Companies and SaaS Team Member management pages
Fixed input focus loss in Edit Recipe when typing component amounts
Fixed numeric input values from changing with scroll events
Fixed Nutrition Facts Panel flicker by removing delayed re-initialization and updating synchronously
Release
UpMeals_7.10.0
Added
Added extra columns to Recipe table to support more robust recipe matching in importer
Fixed
Fixed costing data not loading for recipe components if the user navigates from another recipe
Fixed inaccuracies in costing data tab
Fixed letter casing in grouped sugars, spices and herbs in label data
Fixed tiny icon for Verified Library in recipe/ingredient management screens
Fix translations not being saved for Recipes and Ingredients
Fixed manufacturerSpecs field name in ingredient edit component
Fixed issue with screen freezing when duplicating an ingredient
Fixed some issues with CFIA compliant logic for grouped Sugars and Spices
Fixed Orders management page layout issues and updated styles
Release
UpMeals_7.9.2
Fixed
Saving an ingredient procurement was not invalidating the cache for the ingredient
Fixed incorrect weight calculations in label data
Fix wrong total weight for recipes in costing tab
Added
Playwright e2e testing implemented
CI now runs quick “smoke” checks on pull requests and the full test suite when promoting to staging
On failures, detailed test reports and diagnostics (screenshots, videos, traces) are captured for faster triage
Release
UpMeals_7.9.1
Fixed
Fixed issue where MC shop for guest invitees was getting stuck
Release
UpMeals_7.9.0
Added
Icons for recipe data sections (Untitled UI SVGs)
Color system for input chip categories in Details tab
New "New" badge for recently created recipes and ingredients
Changed
Simplify the success confirmation screen for the recipe importer
Several UI changes to the recipe review screen
Implement default ratio of 1:1 for ml to grams in recipe weight estimations
Improved errors UI in Edit Recipe and Edit Ingredient components
General style updates across the recipe edit component
Updated UPC/Barcode styles and changed download icon; refactored block structure in Label Data tab
Removed unused tag categories for recipes
Automatically sort by newest items after a recipe or ingredient import
Big style update for Workflow screens
Fixed
Fix cancelling recipe and ingredient importer tasks
Fixed problems with recipe importer UI getting stuck before completion
Fixed problems with customers' meal lineup generation and cache
"Needed" field in workflows is not being populated
"Existing stock" field always shows "0" in bulk worklist
Fixed bulk worklist total rounding
Dropdown width in Recipe Importer Review module
Pagination and filters were lost upon page refresh for most management screens
Unauthorized error message when attempting to save an imported recipe
Error when trying to restore a deleted user
Release
UpMeals_7.8.1
Changed
Removed required address validation when adding a payment method
Fixed
Fixed issue in Modern Catering where users invited to a meeting by email were getting server errors
Release
UpMeals_7.8.0
Changed
Updated styles in product view/edit component
Fixed
Fixed sidebar not auto-closing on mobile after navigation or opening importers
Removed disabled "navigating" state on pages
Ingredient Importer error handling fixes
Release
UpMeals_7.7.1
Added
Added caching for Meal Plan order fetching to greatly reduce DB calls on Meal Plan pages
Fixed
Fixed totals calculation for Modern Catering shopping cart
Fixed issue where meeting details could not be edited after cutoff time
Release
UpMeals_7.7.0
Added
Redis Caching System
Enterprise-grade caching infrastructure with TLS encryption for production environments
Dramatically improved performance for database queries and complex calculations across all modules
Multi-tenant cache isolation ensuring complete data separation between organizations
Intelligent cache invalidation automatically maintains data consistency when entities are modified
Cache warming and performance monitoring with metrics tracking for operational visibility
Graceful degradation ensures uninterrupted service when cache is temporarily unavailable
Optimized cost calculation caching greatly reduces computation time for recipe pricing
UI Improvements
Added navigation toggle button on desktop as well as mobile
Changed
Style improvements in recipe and ingredient management
Style improvements in Dashboard and date range selector
Replaced Verified Library toggle switch with toggle icon button
Revamped product image uploader component, added drag & drop functionality
UI revamp for edit customer component: visual refresh, integrated modern phone input, address input and multi-email input components
Hid unused QBO fields
Fixed
Fixed user edit tenant selector UI, replaced broken checkboxes with multi-select dropdown
Fixed order modal footer button alignment issue
Fixed Moden Catering style issues in dashboard, header, meeting and team member pages
Fixed minor UI bugs: button alignment issues, notification dropdown copy, hide procurement actions menu if empty
Release
UpMeals_7.6.0
Added
Ingredient importer widget in welcome dashboard
Added new tenant properties to filter specific UI elements that are currently shown only to the UpMeals tenant:
showQboOptions, showMcOptions and showAdvancedOrderingOptions
Changed
Updated AI recipe importer API host to use new ELB endpoint
Fixed
"Clear Import Runs" debug feature (ingredients and recipes) now clears only for the current tenant scope
Fixed issue where empty rows would sometime persist in management tables after using search
Hide "Serving size" field in FDA/CFIA NFP components when displayed in sub-recipes since it's redundant with the Yield field
Fixed styling issues in filter dropdown component and workflow button-dropdown components
Removed all "Verified Library" and "Edited" label from recipe and ingredient lists
Fixed welcome dashboard to hide "Set a production schedule" for internal production tenants and update onboarding progress accordingly.
Release
UpMeals_7.5.6
Changed
Modified OTP generation logic so times can be easily customized
Set custom OTP expiration times: 7 days for MC welcome emails, 60 min for password resets,
15 min for MFA codes and defaults to 24 hours for everything else
Release
UpMeals_7.5.5
Fixed
Remove "server unreachable" toast when some requests failed due to network issues or server dropping connections
Implement retry logic for GET requests that fail due to unknown reasons
Release
UpMeals_7.5.4
Added
Add option to hide and toggle filters in Production Management screen
Add unsaved changes indicator in Production Management screen
Display savings percentage when switching to annual billing in pricing cards.
Changed
Parse new procurement data returned by recipe importer API
Fixed
Fixed changes detection in Recipe screen while data was being loaded in the background
Prevent form submission when pressing the Enter key on Google Places autofill address field in onboarding
Initialize pricing page currency based on active subscription, tenant address, or IP geolocation.
Fix multiple price display errors in pricing page
Release
UpMeals_7.5.3
Fixed
Fixed error when saving meals related to invalid nutrition data
Release
UpMeals_7.5.2
Added
Tenant information display functionality for super-admins in ingredient and recipe management components
Added 'Fix Recipe Versioning' cron job to supported jobs list for automated recipe versioning fixes
Changed
Refactored tenant switching logic to preserve sub-tenant selection
Increased default max running time in IngredientImporterService from 15 minutes to 1 hour
Refactored recipe category filtering logic in KplDataSourceService
Fixed
Recipe components list sometimes not showing due to change detection issue
Fixed sub-tenant management logic to better handle purposeful selection during tenant operations
Release
UpMeals_7.5.1
Fixed
Ingredient importer timeout issue fix
Nutrition data per component now scales according to custom yield in sub-recipe view
Fix error in MC onboarding flow
Release
UpMeals_7.5.0
Added
Ingredient procurement review functionality with confirmation and swap actions
Default procurement logic for ingredient cost calculations
Comprehensive ingredient data generation including nutrition data, allergens, and tags
Streaming support for ingredient importer service with progress tracking
Background processing for newly imported ingredients
Changed
Enhanced ingredient importer service with tenant-aware matching and versioning support
Refactored BaseMultiTenantRepository to improve tenant isolation handling
Updated ingredient, product, and recipe controllers to prevent relationship overwriting during versioning
Improved ingredient management UI with better styling and data attributes
Enhanced recipe creation process with automatic component versioning
Fixed
Fixed cost calculation issues with ingredient importer
Fixed navigation issues with ingredient importer
Improved progress tracking for ingredient importer
Fixed logic for tenant-specific entity versioning
Fixed Recipe Importer, was calling deleted endpoints
Fixed UI for tenant switcher dropdowns
Release
UpMeals_7.4.0
Fixed
Fix logic for tenant-specific entity versioning
Fixed Recipe Importer, was calling deleted endpoints
Fixed UI for tenant switcher dropdowns
Release
UpMeals_7.3.0
Added
New "Create" button in top bar (look for the "+") for quick access to creating new Meals, Sub-recipes, Ingredients, Orders and Team Members
Added addedSugars to nutrition data model and calculations for enhanced nutrition tracking
Added complianceTag property to Ingredient model and related services for better compliance management
Changed
Refactored top-bar and related components for better structure according to current components
Enhanced ingredient importer with streaming support for improved performance
Enhanced recipe importer error handling and display for better user experience
Enhanced ingredient edit component with improved procurement functionality and styling
Enhanced ShoppingCartService to skip service items during item consolidation
Refactored controllers to extend BaseMultiTenantController for enhanced tenant management
Refactored BaseMultiTenantRepository to improve tenant isolation handling
Refactored nutrition data search component and enhanced ingredient edit layout
Removed automatic polling from RecipeImporterService, now polling only when explicitly needed
Removed stock auto calculations from worklists
Updated cron job schedules and logging in email and tenant sync processes
Updated Sentry configuration for different environments
Updated addedSugars display logic in FDA NFP component
Removed explicit "Nutritionix" reference in edit recipe component
Fixed
Fixed UI issue with dropdowns that were too narrow for the recipe ingredient selector.
Fixed weird jumpy tooltip behavior on "X" close button in some modals
Fixed error in onboarding related to auto-location-detection
Fixed issue where versioned ingredients were ending up with no procurement or wrong procurement data
Disabled auto retry of stripe subscription payments
Added backend connection to Team Member status fields, which were just placeholders
Release
UpMeals_7.2.0
Added
Sentry Error Monitoring and Performance Tracking
Complete Sentry integration with browser tracing and automatic error reporting
Multi-environment configuration with environment-specific trace sampling rates
Release tracking tied to application version
Environment-specific error filtering and alerting in Sentry dashboard
Ingredient Import System Enhancements
Advanced ingredient import functionality with file upload support and progress tracking
Import options for adding new ingredients, updating costs, or both operations
Automatic label compliance tagging for newly imported ingredients
Cost sync and ingredient generator API endpoints integration
Product Cache Management
Automated weekly product cache regeneration via cron jobs
Manual cache regeneration with progress tracking for administrators
Enhanced cache invalidation and relationship handling for multi-tenant environments
Improved performance and reliability for product data management
Multi-Tenant and Settings Enhancements
Tenant-specific product versioning system for better isolation
Database configuration management for recipe importer services
Enhanced global settings with sensitive settings protection
Meal plan settings component integration
Recipe importer URL and port configuration in general settings
Kitchen Production Improvements
Support for cutoff day and reminder day configurations
Enhanced ingredient handling with better error logging
Better tenant isolation bypass options for kitchen production data
Logging Enhancements
Integrated Errsole logging provider with centralized error dashboard
Fixed
Fixed timezone handling across Company and LocationDetail components
Resolved dropdown functionality issues in Modern Catering interface
Fixed recipe weight calculation to include child recipe weights
Corrected search functionality in Recipes Management component
Fixed serving size formatting for FDA nutrition labels
Improved ingredient grouping logic for label compliance
Fixed relation resolving in tenant-specific repository queries
Enhanced error handling for Stripe integration and subscription sync
Fixed navigation logic in Home component for recipe importer access
Resolved tenant switching errors with graceful fallback handling
Fixed packing slips and production day date issues
Hide sub-tenant switcher for global admins
Fixed audit logging for sub-tenant settings
Fixed multiple bugs in Locations settings page including improved deletion confirmation dialogs
Fixed pagination in Companies page with LocalStorage preferences and proper page navigation
Fixed columns data in the Companies page for subscription management
Fixed filters in Companies page with enhanced search and filter handling
Fixed a leak in audit logs when generating user sessions - prevented garbage logs from being created for authenticated requests
Fixed logic in audit logs with nested objects being ignored in the change detection logic
Implemented deduplication logic on ingredient listings
Fixed multiple delivery items being added to meeting orders
Fixed ingredient procurement logic for versioned ingredients
Changed
Performance Optimizations
Enhanced paginated table component with skeleton loading support
Improved ingredient repository to use async auto-tagging
Streamlined date handling across components using DatesHelperService
Optimized kitchen production controller for better data retrieval
Technical Infrastructure
Refactored repository structure to use BasicRepository for better tenant context
Enhanced subscription selection logic for better accuracy
Improved version handling and hotfix detection in app updates
Updated maximum error size configuration for component styles
Consolidated tenant management using TenantHelperService
User Interface Improvements
Enhanced ingredient importer UI with import options and progress updates
Improved two-column layout and upload zone design
Added skeleton loading states for better user experience
Hidden subscription banner for global administrators
Updated ingredient management with dropdown actions menu
Major UI improvements to ingredient edit screen
Release
UpMeals_7.1.0
Added
Complete SaaS Platform Transformation
UpMeals has evolved into a full Software-as-a-Service platform with subscription management and multi-tenant architecture
Self-service company onboarding and management capabilities
Comprehensive billing and subscription management with Stripe integration
Tenant isolation ensuring complete data separation between organizations
AI-Powered Recipe Import System
Upload recipes in any format: PDFs, Word documents, Excel spreadsheets, scanned images, or handwritten notes
AI automatically extracts ingredients, sub-recipes, quantities, units, and nutrition information
Intelligent confidence scoring highlights items that need manual review
Batch processing with real-time progress tracking
Seamless integration with existing recipe and ingredient databases
Review and adjustment interface for fine-tuning before saving
Complete SaaS Management Interface
Company Settings: Manage business information, logo, contact details, and company-wide configurations
Location Management: Create and configure multiple production locations with detailed settings including:
Production schedules and cutoff times
Delivery areas and fees
Kitchen production day configurations
Address validation with Google Places integration
Team Management: Comprehensive user and role management with:
Role-based permissions and access control
Multi-location user assignments
Quick invitation system with bulk email capabilities
User status tracking (active, pending, inactive)
Billing & Subscriptions: Full subscription lifecycle management including:
Plan selection and pricing tiers
Usage tracking and billing portal integration
Trial period management
Automatic subscription renewals
Integrations Hub: Centralized third-party service management
Categories Management: Organize and maintain business-specific categorization systems
Enhanced User Experience Components
Smart address input with Google Places autocomplete and validation
International phone number input with validation and formatting
Secure password creation with strength indicators
Professional image uploader for logos and profile pictures
Multi-email invitation system for team onboarding
Enhanced form validation and error handling throughout the platform
Advanced Multi-Tenancy Features
Tenant switching capabilities for users managing multiple organizations
Tenant-specific customizations and feature sets
Optimized database queries for improved multi-tenant performance
Isolated data storage ensuring complete security between tenants
Modern Catering Enhancements
MFA (Multi-Factor Authentication) support for enhanced security
Email verification codes for secure login
Improved meal plan management with better user controls
Enhanced order management and status tracking
Fixed
Comprehensive improvements to QBO (QuickBooks Online) sync reliability and error handling
Enhanced export functionality across all workflow screens
Improved sidebar navigation and dashboard widget stability
Better handling of timezone calculations and date management
Multiple fixes to email notification systems
Improved meal plan order processing and status management
Enhanced kitchen production list accuracy and performance
Better error handling and user feedback throughout the platform
Changed
Platform Architecture: Transitioned from single-tenant to multi-tenant SaaS architecture
User Interface: Modernized design with improved accessibility and responsive layouts
Security: Enhanced authentication and authorization systems
Performance: Optimized database queries and improved loading times
User Management: Streamlined user roles and permissions system
Billing: Integrated subscription management directly into the platform experience
Release
UpMeals_7.0.1
Fixed
Fixed export dropdowns in workflows
Fixed sidebar menu
Fixed dashboard widgets
Hide MC link for users with no MC customer locations
Only show a list of companies the user has access to under Companies
Many fixes to QBO sync logic
Release
UpMeals_7.0.0
Added
Multi-tenancy enhancements
Enhanced tenant isolation and data separation
Improved tenant switching
Better handling of tenant-specific configurations
Support for tenant-level customizations of features and settings
Optimized database queries for multi-tenant operations
AI Recipe Importer
Supports uploading recipes in various formats including PDFs, Word documents, spreadsheets, and scanned pages.
Utilizes AI to extract key details such as ingredients, sub-recipes, amounts, and units.
Provides a review and adjustment interface for fine-tuning ingredients and amounts before saving.
Integrates with existing recipe and ingredient databases to ensure consistency and accuracy.
Offers progress tracking for recipe processing, with detailed feedback on the number of recipes processed.
Implements a confidence score system to highlight items needing review.
Added components with full UI and front-end functionality for SaaS management screens:
User profile
Company settings
Locations / Location detail settings
Team / Team member edit
Billing & Subscription / Pricing
Integrations
All of them with modular styles, data-cy attributes for testing, unit tests, and JSDoc comments
Created new shared components for reusable UI patterns:
Address input w/ Google Places search
Phone input w/ Google libphonenumber validation and formatting
Password input / New password creation
Image uploader component for settings pages
Multi-email send invitation
Fixed
Fix meal plan order details template bug where divider lines were being displayed incorrectly
Release
UpMeals_6.11.20250503
Fixed
Improve QBO product sync error handling and fallback strategy
Release
UpMeals_6.11.6
Added
Recipe import screen for uploading multiple files
Fixed
Fixed filter bar layout on small screens for order management screen
Fixed wrong email notfication being sent to organizer for meeting cancellations
Fix wrong template ("Meeting modified") being sent to Account Manager when an Organizer Meeting is created
Fixed QBO product sync
Added new field in Product Lineups and Customers for a default QBO Sales Account Name
Improved sync, retry, and error logic.
Removed limitation of 2 uppercase characters to the QBO prefixes. They can be up to 16 characters, upper & lowercase, numbers, spaces, dashes and underscores.
Release
UpMeals_6.11.5
Fixed
Fix endless loading spinner on MC dashboard for Meal Plan admins
Allow applying a promo code to an existing Organizer Meeting order
Prevent sending email invites if meeting settings have invites turned off
Fix broken email notification e2e tests
### [UpMeals_6.11.4] - 2025-03-24
Fixed
Prevent ingredient usage report from failing if costing for one or more ingredients failed due to data
Fix qbo invoice sync when there are promo codes applied to meeting
Changed
Improve reports error handling by providing a warning banner with list of errors
Release
UpMeals_6.11.20250315
Fixed
Fix bug that miscalculated next production day for a given date.
Release
UpMeals_6.11.3
Fixed
Fix cutoff calculation when DST changes
Fix join meeting functionality when auto suggestions don't fit the budget and allergen constraints
Fix cost calculation for ingredients with a processed amount
Improve error handling for ingredients with costing data errors
Release
UpMeals_6.11.2
Fixed
Fixed no totals or tax amounts on some order email notifications
Release
UpMeals_6.11.1
Added
New progress ring component for reports
Custom progress text for reports
Fixed
Bugs in table sorting for some reports due to null data
Batched order processing to avoid database overload and memory warnings
Improve progress task tracking to optimize db calls
Order delivery date incorrect when logging in from a different timezone
Fixed timezone conversion in date helper methods to properly preserve wall time when converting between local and app timezones
Fixed logic for processing meal plan pre-auths
Release
UpMeals_6.11.0
Added
New Reports screen
Ingredient usage report
Sales by Customer report
Sales by Product report
Margins by Customer report
Margins by Product report
Production volume report
New generic reports table component
Configurable columns, saves user preferences
Sortable by any column
Automatic excel export
Individually selectable rows to include in export
Fixed
Fix date range filter in Invoices screen
Release
UpMeals_6.10.20250224
Changed
Improve cron job admin panel
Fixed
Ignore par stock calculations for pre-execution worklists
Release
UpMeals_6.10.0
Added
Dashboard now has configurable widgets and date range selector
Widget for Production Total over time
Widget for top products by category
Widget for product popularity
Widget for internal production log
Release
UpMeals_6.9.20250220
Added
Added task progress tracking for long running tasks in backend/frontend
Fix secondary labels for meeting guests who joined via link
Release
UpMeals_6.9.2
Added
Ability to cancel all meal plan orders for a specific day (Admins)
Ability to cancel own meal plan order for a single day inside the meal plan order details modal
Fixed
Fixed bug with totals in Execution worklist when printing
Fixed totals being zeroed out in worklists when applying filters
Release
UpMeals_6.9.1
Changed
Compress KPL historic data to reduce database size and improve performance
Fixed
Fix filtering of rows in kitchen production list (wrong totals per date)
Fixed white box bug on MC header
Made ingredient procurement not required
Fixed some broken e2e tests and improved test selectors
Fixed loading holidays for current year
Fixed a bug in order date picker logic, where some holidays were not being identified
Release
UpMeals_6.9.0
Added
Added option to have Multi-Factor Authentication (MFA) for login
Added email verification code to MFA login
Added resend code option to MFA login
Allow editing Par and Stock in Daily Prep and Bulk worklists
Any recipe can have par now
Existing stock is auto-calculated based on par and past production usage
Stock is stored for each production day record
Fixed
Fixed bug where invoice date wasn't being updated when editing an order
Fix bug where multiple under-minimum orders were being incorrectly included in invoices
Fix some worklist bugs
Release
UpMeals_6.8.2
Changed
New look and feel for kitchen production lists
Changes to edit customer screen for internal customers
Internal customers cannot have locations. All customer locations will be deleted if converting an existing customer to internal.
Hide some unrequired fields
Set some default values for internal customers
Allow worklists to be filtered by internal customers
Fixed
"Sold as" list on meal costing screen was not working as expected
### [UpMeals_6.8.20250123] - 2025-01-23
Fixed
Fixed error when editing production day settings (eg. cutoff time) that caused production day historic data to be deleted
### [UpMeals_6.8.20250122] - 2025-01-22
Fixed
Orders being assigned to existing invoice when duplicating
### [UpMeals_6.8.1] - 2025-01-21
Added
Added allergens to recipe export data
Fixed
Remove shipping cost from invoice for B2B orders
Include invoice number in packing slip and packing summary export
### [UpMeals_6.8.20250120] - 2025-01-20
Fixed
Wrong delivery totals for meal plan checkout screen
Release
UpMeals_6.8.0
Added
New Invoices entity, management screens
Streamline and update QBO sync for orders/invoices
Sync Stripe transactions to QBO for Modern Catering payments
Lots of changes to orders and invoices
Mailchimp third-party integration with users and guests
New 'Shop Stickers' tags for meals. Display them on the modern catering shop products cards.
New Production Management screen for internal orders
Daily order management with week-based navigation
Search and filter products by category
Copy orders between days within the same week
Copy all orders from previous week
Clear all quantities with one click
Production notes per order
Changed
Made sidebar menu sticky
Only sync one QBO invoice per meal plan week
Made QBO sync error handling and retry logic way more robust and scalable
Update styles for looged out join meeting link
Fixed
Prevent app from deleting QBO products when removed from lineup
Prevent timezones from sometimes being wiped out from db due to failed 3rd party API call
Fix minimum order amount validation for organizer meetings
Fix total order items calculation - now only counts product items
Fix invite email being sent to meeting organizer
Release
UpMeals_6.7.0
Added
Configurable tax rate global setting
Fixed
Sometimes after duplicating a B2B order, creating a new order would pre-populate the duplicated order data and wouldn't allow editing.
Release
UpMeals_6.6.0
Changed
Improve customer switcher for superadmins when going to MC
Fixed
MC Customer admins were unable to edit meal plans due to unnecessary data and screen refresh
Release
UpMeals_6.5.0
Added
Allow to log out from the current device only, all devices, or all devices but the current one.
Changed
New look and feel across the app!
Font changes
Action buttons redesign
Sidebar revamp!
Added new values to meeting notification to sales (budget_per_person, meeting_mode and max_budget)
Release
UpMeals_6.4.1
Fixed
Fixes to 'opt-out from external invoicing' feature
Changed Secondary Label date to shorter format to avoid them getting truncated in the labels
Release
UpMeals_6.4.0
Added
Share session between sub-domains, for tenant switching
Users can now have different tenants open in different tabs without interference
Added "Auto-recommend" button and animation to the meeting product swap modal
Allow customers to be opted out of external invoicing such as QBO
Changed
Enforce tenant subdomain navigation
Links and CTAs in email notifications for tenants (other than the default Vancouver tenant) now include the tenant subdomain
Implemented deep linking to open product modal from productId query parameter in shop page
Redesigned meeting cart totals section for improved clarity and user experience
Redesign join meeting dialog to display additional meeting information and organizer notes
Fixed
Email notification for under-minimum meetings was missing some data
Fixed validations for under-min meetings
Cart was sometimes not cleared when placing meal plan order with full subsidy
MC shop products didn't refresh when switching customers (superadmins)
Fixed error after saving a recipe/meal that prevented further changes
Release
UpMeals_6.3.0
Added
Added button to generate Smart Vending forecasted orders from orders page
Add a new field "organizer notes" for new meetings in modern catering
Changed
Removed "Go to corporate ordering" from top bar and sidebar in Modern Catering, for users that can access both MC and B2B
Added a sidebar link for "B2B Dashboard" instead
Fixed
Forecasted orders cron job improvements to logic and logging
Add the nutrition data to mobile product details in MC
Packing slips templates tweaks
Release
UpMeals_6.2.0
Added
Integrate in-app notifications with Sendgrid email notifications
Revamp Meal plan reminder notifications
Send in-app notification to participants 3 days before cutoff
Send email reminder 1 day before cutoff
Implement expiry dates for notifications
Changed
Remove extra spaces from promo codes to avoid errors
If meal plan cutoff has passed and user has an existing shopping cart, we now inform the user that their order is now for a different week and allow them to proceed with their order
Release
UpMeals_6.1.0
Added
Notifications to customers if pre-auth and payment fails for invitee mode Meetings
Changed
Eliminate loading time before showing meeting mode options for a new meeting
Fixed
Fix meal plan cart errors when team members first click on "order now"
Fixed missing password validation in profile and account pages
Fixed invite mode Meetings not being cancelled if payment was unsuccessful
Fixed the dropdown width for meal options so it now matches the length of the longest meal name in the list
Release
UpMeals_6.0.3
Fixed
Fixed invoice sync issues for meal plans
Release
UpMeals_6.0.2
Changed
Add PO # to order view and Packing Slip
Fixed
Allow internal roles to place orders on holidays.
Fix next-business day logic for holidays
Release
UpMeals_6.0.20241101
Fixed
Errors with DST date calculations in some ordering screens
Release
UpMeals_6.0.1
Added
Show Stripe transactions in the Order details page
For Meal Plan orders, show all the week's orders in the order details page.
Fixed
Prevent auto-recommendations when joining meeting and saving preferences, if it's more than 24h before cutoff
Resolved issue where the "order now" button appeared in the dashboard even after the user had made their selections.
Updated the B2B order creation process to make the next business day after a holiday available for ordering in the datepicker, regardless of the customer's usual ordering days.
Corrected the display of multiple instances of the same product in the secondary worklist table.
Fixed the UPC being displayed in the Product view. It was missing the check-digit.
Release
UpMeals_6.0.0
Added
New Shareable Links for Invitee Meetings
Every invitee mode meeting now includes a link to be shared, allowing guests to join by provinding their name and (optionally) email
Organizers can set the maximum number of allowed guests
Remove beverages from meeting auto-recommendations
Auto-recommendations now include one main dish and up to 2 treats if the budget allows
Release
UpMeals_5.11.0
Changed
Changes to UI and backend to eliminate long loading times when placing meal plan orders and deleting team members
Release
UpMeals_5.10.0
Added
Added holiday management to prevent ordering on holidays
Added delivery window for meal plans edit/view modes, order detail view mode, packing slip and meal plan notifications
Changed
Code changes for new role "Account Manager" replacing "Sales" role
Release
UpMeals_5.9.0
Added
Added Delivery time window for meeting creation and meeting order confirmation emails.
Added cutlery indicators to packing slips
Added select all checkbox to all filter dropdowns
Fixed
Fixed price data in product lineup for a customer
Remember user choice for Active and Verified filters in Meals and Sub-Recipes screens
Filter dropdown closing on every click
Show correct error message while creating B2B order for a customer that has no product lineup assigned
Fixed order status filter dropdown
Fixed promo code limit per user restriction not working as expected
Changed
Minimum order amount for Organizer Meetings is post-taxes now
Release
UpMeals_5.8.20241017
Fixed
App crashes sometimes when creating new B2B order
Release
UpMeals_5.8.1
Added
Added signifi product sync for single and multiple products
Added placeholder text 'No items for this day' if no meal plan selections exist for a day
Added menu option to delete Order for superadmins and Sales
Added an option to waive under-min fees for specific customers
Changed
Limited max item qty to 99 when choosing meal plan selections
Meeting delivery window now doesn't default to any value during creation
New layout for shoping page (Main navigation, filters and cart)
Fixed
Fix weight in nutrition data for subrecipes with a yield of multiple portions
Corrected the issue where "no limit" text appeared in the dashboard widget when no subsidy was provided for a meal plan.
Fixed min order amount to include tax during validation
Release
UpMeals_5.8.0
Added
Implemented meal plan grouping by day for cart and checkout screens
Changed
Change columns in packing summary export to match requirements from Circuit routing app
Fixed
Clear delivery date while duplicating B2B order
Fix worklist snapshot cache for speeding up worklists
Resolved a timezone issue in the meeting readonly view that was preventing meeting edits.
Fixed undefined meal plan name generation for single locations.
Fixed Qbo Invoice # not displaying in order readonly view
Fixed double subsidy text in meal plan order detail view
Release
UpMeals_5.7.20240924
Fixed
DB and logic changes to reduce database usage related to historic worklists snapshots
Release
UpMeals_5.7.0
Added
Meeting join links for MC meetings
Kitchen worklist historic snapshots
Implemented meal plan cancellation and reactivation emails
Changed
Change sorting for packing slips and packing summary to be: Customer -> Location -> Date
Fixed
Fixed a bug that sometimes caused a duplicate QBO invoice to be created for a meal plan order
Fixed NFP daily value percentage discrepancy in NFP pdf
Release
UpMeals_5.6.20240920
Fixed
Missing Meal Plan order details for meal plans with only Saturday enabled
Release
UpMeals_5.6.0
Added
Created endpoints and cronjob to disable signifi's expired lanes using their webhooks.
Added CFIA compliant rounding values for Meal's readonly view across the app.
Added new backend endpoint to return NFP HTML for a meal which will be used for signifi.
Fixed
Fixed yield multiplier defaulting to 1 for ingredients leading to incorrect calculations in NFP.
Fixed translation bug still not showing up as verified for meals.
Changes to meetings controller and cronjob to fix some order placing issues
Fixed show inactive input for location if there is only one when creating new order
Team members user table horizontal overflow for mobile devices
Release
UpMeals_5.5.0
Added
Meal Plan participants snapshot for accurate past order history and participation stats
Fixed
Fixed unnecessary Smart Vending warning emails being sent for Calgary tenant.
Fixed translations for meals not showing as verified
Removed search and category filter in readonly view of order modal.
Fixed save button not getting enabled even after changing location name in edit customer modal.
Release
UpMeals_5.4.0
Added
Add the 'Sales Agent' role to the MC related notifications (orders placed, new meetings, new meal plans)
Added warning dialog when user removes 25% or more of a meeting's invitees
Changed
Disallow deleting meal plans, and implement cancelling and re-activating instead
Allow user to login with username or email in a case insensitive way.
Fixed
Creating ingredients wasn't working due to a recent update (caught by E2E), fixed
Fixed typeahead not working when selecting multiple items in select dropdown
Fixed image uploader layout issues for customers edit view
Hide image placeholder for customers with no logo at customer view layout
Fixed entity not found error when opening a meal plan order created by deleted user.
Don't process forecasted orders and don't send any emails if tenant doesn't have locations with active smartvending machine ids.
Fixed cancel button not working when editing meal plan
Fixed image names when uploading company logo in MC and app dashboard.
Fixed promo code expiration date discrepancy between promo codes list and promo code view.
Fixed bug where we are sending meal plan order cancellation email instead of meal plan cancellation email.
Release
UpMeals_5.3.20240816
Fixed
Hotfix for logging issues for smartvending forecasting orders.
Release
UpMeals_5.3.0
Added
Added single dialog with 2 buttons for both packing summary and slips
Added cronjob run log endpoint to have API access to cronjob logs
Fixed
Fixed same-day delivery orders discrepancy between packing summary and packing slips
Fixed company name not showing up in invitation email when super admin creates and invites meal plan participants.
Fixed CEO and sales getting duplicate meal plan created emails when creating meal plan.
Fixed B2B order notes not displaying in order readonly view, confirmation stage and packing slips
Fixed column widths in packing summary report and made packing slip preview under print dialog hidden.
Fixed the "bowls" icon in Modern Catering
Backend changes to dates calculation to fix some issues with cron jobs
Fix translation bugs where translated content was being saved to wrong fields or even wrong entities
Release
UpMeals_5.2.20240814
Fixed
Improved worklist performance and reduced server workload by removing extra api calls
Order queue logic fixes to fix a rare case where the queue could hit a deadend or endless loop
Backend changes to meeting picks so they are recoverable if deleted
Release
UpMeals_5.2.20240808
Fixed
Internal server error sometimes triggered when editing users
Release
UpMeals_5.2.20240803
Fixed
Team Members having issues placing meal plan orders
Release
UpMeals_5.2.0
Added
Added a queueing system to prevent Meal Plan, Meeting, and B2B orders placed at almost the same time from causing data issues.
Send meal plan order cancelled email to participant who cancelled the order.
Added 'position' field to team member importer.
Added new order notes field to order view which is displayed only in edit mode.
Fixed min order amount defaulting to $125 if location has it set for $0
Change 'Delivery Time' to 'Delivery Window'
Added resend meeting invitation functionality for invitee meetings for users who had not confirmed their selections.
Fixed
Fix logic that calculates cutoff for new or modified orders
Addresses that have a postal code that Google Maps API doesn't recognize were not being allowed to finish MC Onboarding
Fixed username getting replaced with email when creating user.
Set the first payment method added to user or company as default.
If a default payment method is deleted, set another one as default if there are any.
When user is making meal plan selections, don't show company cards.
Fixed meal plan name not showing in batch packing slips and incorrect badge mismatch between meetings and meal plan.
Improved selectors in meeting edit component for higher reliability on e2e tests
Round totals to the nearest cent in the getTotals endpoint
Release
UpMeals_5.1.0
Added
Added delivery time to order view, meeting view, and invite confirmation while creating
Added print packing slip pdf for both single and multiple orders.
Fixed
Removed time from Delivery date for non-meeting orders in order view
Improved order min amount validation
Fixed "User not assigned to current tenant" error when inviting new user
Fixed "500 Error" when login expired
Fix order errors when payment method was missing
Fix handling users who are logged in to tenats they no longer are assigned to
Unable to set a payment method as default after created
Fixed allowing to click on "place order" when a payment method wasn't selected
Fixed multiple console errors
Fixed address validations
Prevent multple clicks in "confirm" button when swapping meeting picks
fix login redirection when user was logged out because of expired login
Changed
Always show QBO invoice ID and document number fields for admins when editing an order
Release
UpMeals_5.0.0
Added
Multi-tenancy features
Tenant-specific MealMatrix databases, switchable in a per-request basis
User can be assigned to multiple tenants and have a default tenant
Users can switch tenants on the fly with a tenant switcher
App settings are tenant-specific
Audit logs are now tenant specific
revamp cron jobs to run as HTTP requests for each tenant
Many UI changes
Many backend changes, including updating libraries to their most current versions
Add Google Places addres completion everywhere we have address input forms
Add B2B serviced cities settings (separate from MC)
Fixed
Packing summary dates now uses new API endpoint to accurately get the date range
Disable datepicker when loading dates in B2B order screen to avoid blank calendar
A false-positive green banner was showing for a few moments in worklists when today is not a production day
Fixed address validation in Onboarding, Customers, and Locations screens
Removed customer selector from Meeting creation screen
Release
UpMeals_4.21.0
Added
Added 'No product line up is associated to your company' dialog in shop page.
Fixed
Fixed multiple instances of product not showing up in secondary label worklist export csv.
Fixed packing summary date range issue where orders beyond next production date are getting exported in excel sheet.
Update order delivery date when meeting date is changed
Release
UpMeals_4.20.20240714
Fixed
MC users export wasn't correctly filtering MC customers
Release
UpMeals_4.20.20240712
Fixed
Bug in meal plan shopping cart where a user could only select one of a single product, due to a runtime error
Release
UpMeals_4.20.0
Added
MC Users CSV export from User Management screen
Changed
New order detail view layout
Fixed
Fixed meeting delivery time range window resetting to 15 mins before meeting time when you start editing a meeting.
Prevent inactive users, or users assigned to only inactive Customer Locations, from accessing REST API, instead throw a user-friendly error and log them out.
Fixed QBO sync error of Cutlery items in meetings
Past Meal Plan Orders with ordered items showing as "No order"
Fixed issue with too many decimal points for dollar amounts in email notifications
Remove location from showing up under "missed orders" section in SV orders review notification based on their stocking days.
Fix Meal Plan order table for mobile screens
Fix checkmark position in the shop sidebar filters + Shop filter bar fixed position for mobile devices
Fixed "All" filter option in mobile shop
Fixed meal plan order totals discrepancies in meal plan my orders tab and meal plan order view modal.
Release
UpMeals_4.19.2
Fixed
Fix bug with Secondary Label Worklist where invitee meetings sometimes had extra labels for products from another meeting
Fixed packing summary production date range bug.
Release
UpMeals_4.19.1
Fixed
Fix bug in invitee meeting orders: product names weren't getting saved in the order items.
Fixed bug that created duplicate Delivery line items in QBO invoices when modifying an Organizer meeting order
Changed
Removed caching of QBO invoices, since it's no longer necessary and it was troublesome to keep in sync.
Release
UpMeals_4.19.0
Added
Added position column to secondary label worklist and also to the csv export.
Changed
Search MC team members by email in addition of name and last name
Moving totals from footer to sidebar in the Order detail View
Fixed
Added backend validations to prevent MC orders from being placed after cutoff time (either Meal Plan cutoff or production cutoff)
Fixed empty company name in meal plan created email template to sales.
Fixed forecasted smartvending orders not getting generated due to shared line ups
Remove inactive and unverified products from the MC shop
Removed inactive or unverfied meals from showing up in B2B products list
Fixed production days logic in packing slip summary export to rely on cutoff dates not production dates
Fix bug when switching shared lineups for a customer, it caused many problems and didn't assign the new lineup correctly.
Fixed layout issues on MC mobile: 1st step create meal plan, dashboard widgets overflow, meal plan layout tweaks and modal fixed max height
Release
UpMeals_4.18.20240702
Added
Added Province Abbreviation field in Serviced Cities, to avoid maps API errors
Fixed
Shipping cost calculation now uses the stored cities instead of external API, because it was causing issues
Release
UpMeals_4.17.0
Added
Added a prompt and a notice for users if their version of the app is outdated and needs a page refresh
Sync products to QBO when editing and saving a Product Lineup
Added new logs while creating Smartvending forecasted orders for more transparency
Added a banner in production worklists that indicates if there are discrepancies between app orders and QBO invoices
Changed
Update Modern Catering icons
New animations and layout for checkout confirmation page for organizer selection meetings
New animations and layout for checkout confirmation page for meal plans meals selections
Fixed
Revamped the logic for placing meal plan orders, to fix a bug with some orders unexpectedly being deleted
Fixed bug where superadmins with no customers were not able to import any team members using team member importer
Fixed sorting order of shop categories and tags
Release
UpMeals_4.16.1
Fixed
Fixed time ranges in packing summary report, change export format from CSV to XLSX
Release
UpMeals_4.16.0
Added
Added functionality to export csv of packing slips summary between previous and next cutoff dates.
Slim version of shopping cart added for small browser windows
Release
UpMeals_4.15.20240629
Changed
Assign MC shared lineup to new customers from MC Onboarding flow
Release
UpMeals_4.15.20240619
Fixed
Fix code that looks up an address by postal code using google's API
Release
UpMeals_4.15.20240618
Fixed
Issues reported with Secondary Label Worklist because of Shared Lineups
Issues found with meetings and meal plan orders due to Shared Lineups
Release
UpMeals_4.15.0
Added
Shared Product Lineups accross customers.
Screens to manage product lineups
Revamp QBO sync to create lineups based on prefix
Revamp orders accross app to reference product IDs directly instead of customer-product relations
Create a script to clean up data generated by e2e tests on every app startup
Release
UpMeals_4.14.20240614
Added
Indicator of over-budget for meal plans with 100% subsidy when user goes over limit
Fixed
Fixed totals in cart and checkout for Meal Plans
Release
UpMeals_4.14.0
Added
Added new meal plan animations
Fixed
Fix promo code and tax calculations in MC
Release
UpMeals_4.13.20240613
Added
Added new Add/edit participants option for meal plans.
Added new Add/Edit invitees option for meetings.
Added logic to divide created orders count, missed orders count and missed customer locations for Monday and Thursday when sending SV forecasted order review email.
Added new setting tab called Smart Vending and added a time field which is used as cutoff time to create forecasted orders
Added functionality to not create duplicate forecasted orders for a location.
New layout for Select your meals Alert when selecting meals for Meal Plans
Changed
Change meeting type images for lottie animation in the Create meeting step 1
Login Prompt new layout and content update
Editing orders linked to a QBO invoice which contains items that are excluded from the app won't overwrite the invoice with only the items from the app, but keep the excluded items in the invoice and sync the ones from the app.
Fixed
Change date time format for Secondary Label Worklist csv export function
Added extra validations for team member csv importer
Release
UpMeals_4.13.20240613
Added
Added new Add/edit participants option for meal plans.
Added new Add/Edit invitees option for meetings.
Added logic to divide created orders count, missed orders count and missed customer locations for Monday and Thursday when sending SV forecasted order review email.
Added new setting tab called Smart Vending and added a time field which is used as cutoff time to create forecasted orders
Added functionality to not create duplicate forecasted orders for a location.
New layout for Select your meals Alert when selecting meals for Meal Plans
Changed
Change meeting type images for lottie animation in the Create meeting step 1
Login Prompt new layout and content update
Editing orders linked to a QBO invoice which contains items that are excluded from the app won't overwrite the invoice with only the items from the app, but keep the excluded items in the invoice and sync the ones from the app.
Fixed
Change date time format for Secondary Label Worklist csv export function
Added extra validations for team member csv importer
Release
UpMeals_4.13.5
Added
Use customer location's address as billing address functionality is added to create/edit location modal.
Added CEO as email recipient in forecasted orders review email.
Changed
Switch kitchen worklists to App orders instead of QBO invoices by default
Make cutlery independent of organizer meeting attendee count.
Fixed
Any changes to promo code is not enabling save button which is blocking user from saving promo code changes.
Fix "Free Delivery" option in promo codes not working
Release
UpMeals_4.13.4
Added
Added approve button in readonly view of order detail (for forecasted Smart Vending orders)
Added logic to send missed machines in email notification to sales during creation of forecasted orders
When attempting to delete a Team Member, show a warning if they belong to any active meeting or meal plan, and if confirmed, cancel any orders and process any applicable refunds before deleting the user.
Fixed
Hide modify order button for draft organizer meetings if meeting date has passed, or it's past cutoff
Fixed promo code discount appearing multiple times for organizer meeting readonly view.
Release
UpMeals_4.13.2024052702
Added
Notification for Sales/CEO/Logistics/SuperAdmins when Meal Plan orders get cancelled due to being under-minimum total
Fixed
Enable email notifications for meal plan creator, customer admins, and team members when meal plan order gets cancelled due to under-min amount
Release
UpMeals_4.13.20240527
Fixed
Bug in Meal Plan order placing cron job that mistakenly cancelled some orders that don't have 'under-min fee' setting on
Release
UpMeals_4.13.3
Added
Tool for checking app orders vs QBO invoices discrepancies in worklists
Added functionality to retry to create forecasted SV orders until all SV orders are successfully created.
Changed
Product data endpoint change: Don't mark Bowls or Treats as Breakfast items
Fixed
Fixed sending cutoff date in meal plan emails in app timezone (Vancouver) instead of server timezone (UTC)
Added functionality to create orders based on SV forecast and email Sales for orders review.
Fixed logic for meal plan orders, to prevent duplicate QBO invoices
Hide delivery location by default and only show for customers with more than 1 b2b location
Fix deleting customer locations in MC when filter/search results is only 1
As a side effect of disabling MC locations in B2B order screen, users were unable to open existing MC orders from Orders page. Fixed the issue by allowing existing MC orders to be displayed, but not either editing them or creating new ones.
Fix wrong Participation numbers for current meal plan week in meal plan orders list
Fixed updated meal plan selections email being sent to users instead of confirmed meal plan selections email
Release
UpMeals_4.13.20240522
Added
Endpoint to fetch product data for SmartVending order forecast
Release
UpMeals_4.13.2
Added
Added delivery time window field in meetings
Add participation info to meal plan order list and details
Added logic to validate postal code when adding new company location in MC
Added indicator for minimum amount in B2B order screen.
Adding sparkles icon to AI generate buttons
Changed
Hide Same day delivery date under checkmark for customer edit view
Date Pickers are now fully clickable, including the icon
Fixed
Only show customers with at least one B2B location in the B2B ordering screen.
Meal Plan orders under-min delivery fee was failing to sync to QBO invoices
Fixed filtering of meal categories while creating B2B order to be limited to products meal categories
New location was not showing in list after adding
Location refresh button didn't work in MC
Team Member count column was empty in location management list
Fix order list tables to overflow the viewport
Fixed delivery date hour format in meeting readonly view
Re-ordering a B2B order now uses updated prices
Release
UpMeals_4.13.20240511
Fixed
Fixed opening "Current" meal plan order in Meal Plans Dashboard
Release
UpMeals_4.13.1
Added
Add the cutoff info and action buttons when a meal plan order is placed successfully
Changed
While creating B2B order, gray out the Next button if the required fields haven't been set (customer location and delivery date)
In B2B order screen, replace label and placeholder from 'customer location" to "delivery location"
Only notify meal plan participants of changes if the meal plan budget or available days change
Update current meal plan week's payment info when edited in meal plan screen
Fixed
Fix broken own meal plan order details modal for "current" week
While in the shop chosing items for a meal plan, a page refresh triggered the "please select meals for meal plan" modal again. Now it only appears right after following the meal plan order link, not with every refresh.
Team members sometimes unable to place organizer meetings
Fixed production day settings UI bugs and added 15 minute time interval dropdown for cutoff hour
Fixed layout issues in Meal Plan modal header
Release
UpMeals_4.13.20240509
Fixed
Fixed an issue where if the team member was the first to order for a meal plan week, it wouldn't let them add the meal plan to the cart
Release
UpMeals_4.13.2024050802
Fixed
Meal plan invitation link for logged out users was not redirecting to meal plan selections
Meal plan settings were shown in the user prompt when ordering for a meal plan. Meal Plan Week settings are now shown instead (current active settings)
Invitation meeting promo code wasn't being applied to the Order and QBO Invoice some times
Fix customer resetting to default one on page refresh for superadmins that are assigned to a customer
Release
UpMeals_4.13.20240508
Fixed
Removed some debug code that was causing QBO invoices to fail syncing
Release
UpMeals_4.13.0
Added
Add option to copy meal plan invite links in meal plan screens
New prompt for Team Members to add shopping cart product to meal plan if cart is empty
Notifications to Sales and dev team every time a sync to QBO fails (customer or invoice)
New functionality to sync individual customer and its locations from QBO
New functionality to save a customer to QBO
Added contact number field for meetings
Changed
Create meeting pre-auths 2 days before meeting cutoff instead of at meeting creation time
Fixed
Fix team members being able to access admin-like UI if assigned to B2B location
Fix team members being able to see all orders for all customers if assigned to a B2B location
Fix meal plan week QBO invoice being included in kitchen production worklists
Fixed secondary label worklist not showing meal plan labels
Fix subsidy totals in meal plan order details
Fix meal plan participation counter in dashboard
Fix error handling when pre-auth fails when saving an invitee meeting
Release
UpMeals_4.12.20240503
Added
Added a "copy invite link" button to invitation meeting details for each invitee
Release
UpMeals_4.12.20240427
Fixed
Fixed meal plan week number calculations
Fixed horizontal scrollbar issue in modals
Release
UpMeals_4.12.3
Added
Added delivery address and placed by fields to B2B placed/updated email templates for sales admins
Added warning alert and message to edit meal plan modal when user edits a meal plan which has orders for current week
Changed
Don't attach QBO invoices to order confirmation emails
Improvements to the multi-email input (for guest invitees)
Removed long description and chef notes to sales
Fixed
Fix validation issues for guest emails in invitee meeting
Fixed sorting of customer names to be in alphabetical order in customer selector alert post login
Fixed all locations appearing in locations filter for superadmins even after selecting a customer, only locations of current customer should be shown
Fixed collapsible subsidy details at checkout
Fixed meal plan reminder being sent at 2:00 am
Fixed can't edit draft meetings after cutoff
Fixed bug where meal plan widgets are not showing properly for team member role and also coming up text bug
Fixed bug where create meal plan button was not showing if there are no meetings in dashboard
Fixed Superadmins can't add company card.
Fixed unsaved changes message showing up even when nothing is changed.
Release
UpMeals_4.12.2
Added
Dialog asking to select a company whenever a multi-company user is using MC
Fixed delete option in ellipsis menu not appearing when only 1 item is present in locations list
Added meal plan widgets to Modern catering dashboard.
Fixed Marketing user, Recipe Manager and other equal level access able to see meetings created by anyone and also not able to see meetings they created in upcoming meetings widget in dashboard.
Added HTML element IDs in Meal Plans to assist with automated testing
Fixed
Prevent two context menus from opening at the same time in payment methods screen
Fixed layout issues when editing items in cart
Fixed multiple scrollbars appearing in Customer Product Lineup tab
Show the company logo in MC for superusers or other multi-customer users
Only show customers that have MC locations in MC customer selector
Fixed some console errors when logging out
Show all companies in the company selector for superadmins belonging to a customer
Fixed console errors that caused automated meeting tests to fail
Fix bug where meeting invitees would get second invite email if they modified their selections and meeting organizer modifies and saves the meeting
Fixed all roles equal to meeting organizer not able to see upcoming meetings in dashboard
Fixed invalid toast when adding a location with empty fields.
Fixed mismatching email template ids for meal plan creation and cancellation
Fixed prices misaligned in checkout for organizer selection meetings
Fixed "past cutoff" meeting notices and tooltips
Fixed responsive issues for paginated tables components
Fix orders cancelled because of failed payment preauth capture
Fixed promo code layout for Organizer Selection checkout
Changed
Revert to QBO loading for kitchen worklists
Updates to checkout modal last step for organizer meetings
Show only "Verified" and "Active" Meals & Sub-Recipes by default
Removed the sparkles emoji from Meal Generator related buttons, updated copy
Default /mc page for logged in users is now the dashboard instead of shop
Enable future production days in Worklists' datepicker up to 2 weeks in the future
Hide select all in team member selector and show 'No team members found' message if search results are empty
Improved global modal vertical layout
Release
UpMeals_4.12.1
Added
Added success dialog to invitee mode meeting
Added dietary preferences tooltip for single picks viewing mode.
Changed
Meal Plan invoices are getting marked as paid when applicable
Fixed
Prevent users with multiple customers (or superadmins) from seeing all meal plans at once
Show only customers that have B2B locations in customer selector, in create B2B order modal.
Fix shopping cart sometimes getting lost on page refresh
Fixed allowing negative values for promo code discount amount.
Fixed not allowing duplicate meal plan names and auto incrementing meal plan name by adding next sequential number.
Fixed date range filter not filtering B2B orders when selecting one date.
Fixed issues with syncing invoices for meal plans to QBO
Release
UpMeals_4.12.20240328
Fixed
Fixed a bug where MC shop wouldn't load for meeting guests
Release
UpMeals_4.12.0
Added
Fetch Kitchen Worklist data directly from App orders instead of QBO
QBO sync option is still available as a failsafe, for the transition period
Added new custom component for multiple email entry in invitation meetings
Fixed
Fixed values and added nutrition data in meal line up UI and export.
Changing production sequence causes recipes to be unverified
Fixed email notification errors
Fixed promo code not clearing after going to different step if there was error while applying promo code in invite meetings.
Fixed triggering welcome email upon editing existing user
Avoid clearing customer selector for multiple customer users
Fixed layout issues in cart totals
Fixed applied promo code layout in Invitation mode cost summary
Fixed meeting datepicker to show next month if all dates are disabled in current month.
Fixed bug that prevented superuser-like users from switching customers in MC
Removed defaulting to isB2B when both isB2B and isMC are not checked for a location.
Fixed Kitchen Manager able to create team member for any customer in modern catering.
Release
UpMeals_4.11.1
Added
Added addtional information (Meeting style + Delivery Address info) to Edit Meeting modal
Fixed
Fixed title and behavior changes of decline button and cancel button for decline meeting dialog.
Sometimes when logging out from the Meetings screen, the app would freeze and the login screen would not be shown
Fixed clearing of customer locations for a location admin upgraded to customer admin.
Fixed unable to add promo code if same promo code was deleted earlier
Fixed Company subsidy and GST labels in cart
Fixed final cutlery fee including declined attendee's cutlery fee.
Fixed missing totals in Swap mode
Fixed bugs with the "navigate away from swap mode" modal
Remove "create an account" link for logged in users, separate words "log in"
Don't allow declined attendees to make selections until rejoined
Fixed multiple scrollbars issue for roles view
Fixed can't invite company admins in invitee meetings
Prevent 403 errors for unauthenticated MC users when navigating to "privileged" MC links. Now it goes to /login instead ad users will be redirected to original url upon successful login.
Fixed label for Update order/Place order button when editing/creating Organizer Selection Meeting
Fixed validations for custom budget amount in invitee meetings.
Release
UpMeals_4.11.20240312
Changed
Separated "login" and "register" links in top bar for unauthenticated users, and changed "register" to "create an account"
Fixed
Fix missing "Sign Up" button in MC-specific login page
Release
UpMeals_4.11.20240311
Changed
Show under-minimum delivery fees in the Meal Plan Dashboard
Fixed
Restore access to /mc for non-authenticated users
Release
UpMeals_4.11.0
Added
Meal Plan dashboard for Admins and Participants
Allow internal user roles to belong to a company/location and use MC
Added logic for conditional label text on the Save button for invitee mode meetings depending on whether you're creating or editing a meeting ("Save changes" / "Save and send X invites" ). It now also displays the number of invites to send.
Added cutlery as line item for draft and live meetings.
Added B2B Order updated template for sales.
Added copy and cancel button changes of rejoin meeting dialog.
Changed
If a user's permissions change, they no longer have to log out and log back in, they can refresh the browser and the app will reflect the new ones.
Allow multiple-customer users (superadmin-like) to switch seamlessly between users in all MC screens
Changed meeting mode name from "Invite Attendees" mode to "Invitation" mode
Updated Pull Request template format to better match our process
Fixed the cost sumary table for better layout
Fixed
Fixed the disabled meeting row behavior for organizer and declinee.
Fixed dietary preferences modal opening sometimes even when user has already set their preferences
Fixed the order of meeting styles when creating new meeting.
Fixed layout for upload file button when importing new Team members from csv file.
Fixed meeting style required validation
Fixed meal plan updated email being sent to newly added attendee
Hide declined attendee meeting recommendations in meeting view.
Fixed dietary preferences refreshing in account only after page refresh.
Fixed totals not appearing until payment method selected.
Fixed meal plan week date range and cutoff date in email.
Fixed rounding errors in meal plans checkout screen
Fix issues in redirections and route authorization for MC and internal roles
Fixed layout: Create new meal modal first step for better responsive behaviour
Release
UpMeals_4.10.0
Added
Added promo codes for invite mode meetings.
Fixed
Fixed users being able to go next step even when payment methods are loading during editing meeting and meal plan
Release
UpMeals_4.9.0
Added
Added automatic weekly B2B Reminders.
Included additional order data in B2B cancellation email for Sales
Include meeting location and delivery notes in QBO invoices
Save products in Organizer Mode meetings for later
Added data attributes to all tables to improve e2e test targeting
Fixed
Fixed company location missing in meeting modified email subject line
Fixed sending correct template to sales when B2B order is cancelled
Fixed price rounding issue in emails
Prevented editing of meeting mode, meeting style and location
Fixed duplicate selected team members in meal plan confirmation screen
Fix API error with Onboarding flow and adding team members
Fix meeting declined bugs, other meeting order bugs
Fixed attendees count showing as NaN
Removed external emails from meetings confirmation step.
Fixed b2b reminder logic not calculating delivery dates for next week
Fixed cutoff lead time in b2b reminder email
Fix shopping cart validation errors (Shopping cart doesn't have a meeting or meal plan bug)
Changed
Changed the way the attendee detail breakdown is displayed in meetings
Updated conditional lead copy for product recommendations in invitee meeting view
Release
UpMeals_4.8.2024012902
Fixed
Prevent sending duplicate invite emails to users that have already confirmed their picks
Release
UpMeals_4.8.20240129
Fixed
Added validation to check uppercase emails in meeting and meal plan attendees
Increased column width for containers in the meal bulk editor
Release
UpMeals_4.8.20240126
Added
Added validation to check container name is unique and atleast one packaging component is added.
Fixed permission issue for recipe manager to edit the container.
Release
UpMeals_4.8.0
Added
Add CEO to Invitee mode meeting created & Organizer select meeting order confirmation emails
Added ability to add quantity for each package in a container.
Added status indicator and filter in packaging component list page
Added status badge and filter to packaging list.
Only send 'Meeting changed' emails when certain meeting fields change
Meeting time, meeting date, location details
Changed
Changed UI component to select packaging components for a container
Fixed
Fixed dietary preferences modal popping up and disabling placing order for guests.
Fixed the invitee count discrepancy
Release
UpMeals_4.7.3
Added
Added CSV importer for smrt1 product ids
Release
UpMeals_4.7.20240119
Fixed
Fix "Invalid Date" bug when editing B2B orders
Add overrideable min order amount for MC Organizer meetings
If minimum order amount for location is zero, it will default to $125
Release
UpMeals_4.7.2
Added
Added delivery day restrictions to stocking days while placing B2B order.
Added ability to associate container to a meal in bulk editor.
Release
UpMeals_4.7.1
Added
Added container management and created association between container and meals
Release
UpMeals_4.7.20240118
Fixed
Hotfix for not sending unnessary 'meeting updated' email to all invitees.
Release
UpMeals_4.7.0
Added
Draft meetings functionality
Meeting is autosaved every time the user clicks on 'next'
Meetings are saved as 'draft' until the user clicks on "save and send invites",
or places an order in the case of Organizer mode meetings.
Final review step just before payment to see the automatic product selections for invitees
Restore deleted user when creating a user with the same email as a deleted one.
Added new fields for SmartVending Stocking Optimization feature
Machine ID in Customer Location
SmartVending ID in Products
Stocking days in Customer Location
Added date created, customer and status badges to users list along with filters and sort functionality.
Release
UpMeals_4.6.1
Added
Added meal plan validations for subsidy percentage and limit amount
Added functionality to redirect pending users to create password screen
Release
UpMeals_4.6.0
Added
Added guest tables functionality to save guest preferences
Removed filter which won't show pending users in team member selector.
Prevent search engines from indexing the Staging app
Completed pending email notifications
Added "meeting mode" column to list of meetings
Added Lottie library to load and play Lottie animations
Added styles to initial Meal Plan creation step + added final success step with Lottie animation
Fixed
Fixed removing single item from meal plan cart removes all items with same name.
Fixed bug which sends too many emails to sales managers
Fixed UPCs not being auto generated in Staging and Production
Fixed Meal with protein and meal timing tags showing as no tags
Release
UpMeals_4.5.1
Added
In MC Shop, prevent search box from hiding behind shopping cart on big screens
Improve MC Shop filters logic and performance
Changed
Change recommendation logic for meetings to include meal timing tags
Fixed
Fix title case in page titles
Release
UpMeals_4.5.0
Added
Option to include cutlery for each attendee in a meeting
Implemented a retry strategy for failed network requests to the REST API. Default attempts is 2.
Added empty cart graphic
Changed
Post MC onboarding, redirect user to dashboard not home.
Changes to login screen links text
Fixed
Fixed meeting timezone issue in meeting emails
Fixed mobile styles for dietary preference modal and swap modal
Release
UpMeals_4.4.20231210
Added
Added meal plan and meeting weekly reminder emails
Fixed
Email notifications errors with logic and data
Wrong attendee count when creating an invitee meeting
Wrong max total of attendee meetings (adding tax unnecessarily)
Release
UpMeals_4.4.20231206
Fixed
Could not save questionnaires, the server returned an error
Release
UpMeals_4.4.2
Fixed
Fixed incorrect meeting time and missing attendees count in invitee meeting order confirmation email.
Updated UI text for excluding beverages in meeting modal
Added
Modal showing result of meal sequence optimization API in Execution Worklist
Added HubSpot embed code
Release
UpMeals_4.4.20231201
Fixed
Hotfix for enabling email while adding user/team-member
Release
UpMeals_4.4.1
Added
Kitchen activity dashboard view
Added Exclude beverages from product recommendations option for invitee meeting.
Removed Beverages, snacks, treats, juices from Preferred meal types
Added a step for guest to provide their name in product swap modal.
Implement displaying day ranges in Meal Plan
Changed
Add the Category filter back to Packing and Reverse Packing worklists
Enabled the Meal Plans card in the first-time dashboard (removed "Coming Soon")
Fixed
Excluded Sunday from the available days in Meal Plans.
Fixed the styling issues in the modal for selecting Dietary Preferences.
Fixed toast styles and made them responsive
Release
UpMeals_4.4.0
Added
Meal Generator
UI: Within the Meals page, click on "Meal Generator" button to use the AI-powered meal generator
Integrate with Meal Generator backend
Label Generator
UI: In the Tags + Descriptions tab in meal screen, click on Generate
Integrate with Label Generator backend
Label generator automatically used when generating a new meal with Meal Generator
Fixed
Improve form logic in /mc/join flow
Sort by Name in Meal Plans
Remove 'Other' option in Meeting Style
Fixed email being sent twice to sales when order is created/updated
Fixed all styles for Meal Plans, big and small screens
Release
UpMeals_4.3.20231120
Added
Include MC orders in Orders screen
Add filter to see only MC or B2B orders
Added B2B order placed/updated, MC meeting invitee cancelled emails and added logic to send emails to sales managers.
Changed
QBO invoices are now generated when MC Invitee meetings are created, as opposed to waiting until cutoff time
Release
UpMeals_4.3.20231117
Added
Added back the Username field in edit user screen (main app)
Fixed
Wrong cutoff date shown in meeting view screen
Release
UpMeals_4.3.20231116
Fixed
Editing invitee mode meetings that have credit card payment was throwing an error
Fix the "nothing to save" issue when editing budget in invitee meetings
Release
UpMeals_4.3.1
Added
Added CSV importer for team members
Secondary Labeling worklist
CSV export for Secondary Labels
Fixed
Consolidate order/invoice lines by qty
Don't mark invoices as PAID when credit terms used
Fix bug that prevented editing meeting details
Fixed incorrect meeting time and missing attendees count in invitee meeting order confirmation email.
Fixed styles for number input controls
General style and responsive fixes
Release
UpMeals_4.2.20231115
Fixed
Bug that caused payment method selector to some times be disabled for team members of companies with credit terms
Release
UpMeals_4.2.20231113
Fixed
Hotfix for showing product picks in past cutoff meetings
Release
UpMeals_4.3.0
Added
Reverse Packing list (packing list grouped by customers)
Changed
Rearranged meeting creation steps and made Meeting style / Expected attendees fields conditional
Fixed
Fix meal plan frequency copy in notification emails
Release
UpMeals_4.2.20231109
Fixed
Logic for new b2b order email recipients - was incorrectly sending the notifications to a test email account
Release
UpMeals_4.2.10
Added
Added new Quick Actions widget to MC dashboard
Added email templates for meal plan cancellation, refunds, welcome emails by role etc.
Added "Beta" indicator on header logo
Changed
Change 'from:' email to '<support@upmeals.ca>', and name to 'UpMeals'
Fixed
Fixed bug where emails were not being sent in Staging environment.
Fixed deleting cart items from the shop's interface in Meal Plan picks mode
Fixed modifying qty of first item in cart, which would cause it to move to last place.
Fix logic for meeting placement cron job
Automatically mark meeting invoices as paid in QBO
Attach invoice PDF to meeting confirmation email
Release
UpMeals_4.2.9
Added
Added pending priority 2 emails for sendgrid.
### Change
The text is changed, and buttons are added to the modal when you access a meeting with the wrong account
### Fixes
If meal plan order total for the team member is less than $0.50 due to subsidies, charge it up to the company and make the order total $0
Editing meal plan picks when the same item was selected for more than one day was causing all items to be assigned to the same day
Fixed another bug in meal plan picks where picks of same item for same day weren't being saved
Fixed logic for adding/removing/editing items in cart for meetings and meal plans
Having an active cart for Meal Plan, and then editing the order for a meeting, the cart would have both entities and the UI would break
The QBO# column is now larger and allows you to see the complete number.
If user had 2 or more identical meal plan picks for a certain day, you could only delete all at once or none at all
Clicking on the trash icon for a meal pick was behaving incorrectly when the item was in more than 1 shopping cart row
Release
UpMeals_4.2.8
Added
Create QBO invoices for weekly meal plan deliveries
Attach invoice PDFs to meal plan confirmation email
Added pending Meeting Sendgrid dynamic templates.
Release
UpMeals_4.2.7
Added
Added error message dialog when clicking on an expired token from an invitation email
Added welcome dialog when entering the shop from a Meal Plan invitation link
Changed
Redesigned meeting creation screen for invite mode meetings
Made firstname, lastname required during MC onboarding.
The MC preferences welcome modal has been modified with new design.
Add total weight to Meal NFPs. Also some small style and copy fixes.
Release
UpMeals_1.0.2022100602
Fixed
Fixed false positives while checking circular dependencies for nutrition data calculations
Release
UpMeals_1.0.20221006
Added
UPC field in Meals instead of SKU
UPC is auto-generated if left blank
New Data Integrity tools to auto-generate barcodes for all meals without one
Changed
Backend auto-generates UPC for new meals, or when saving a meal without UPC
Fixed
Filters and page number were lost when opening a modal in Meals/Recipes screens
Images folder on Staging was wrong, so images do not show after uploading.
This bug was only happening on Staging env.
Release
UpMeals_1.0.20220926
Added
Image upload functionality
Uploads to S3
Uploads 4 versions of the image: thumb, medium, large, original
UI Component for displaying a stored image using any of its 4 sizes
Release
UpMeals_1.0.20220923
Added
Added field validation for user, customer, and lead forms
Updated documentation for Generic Management and Edit screens
New Label Data View, which compiles all data necessary for label creation in one convenient tab. Includes NFP download, barcode SVG download and nifty copy-to-clipboard buttons
Added label management component
Added packaging management component
Added label management component
Added new functionality to UPC code
Added copy to clipboard functionality to barcode, changed download button location
Added warning signs for ingredients that does not have nutrition data
Fixed
Some invalid fields inside forms had their font size reduced when displaying validation errors
Release
UpMeals_1.0.20220907
Changed
Change values for meal cost percentage thresholds
Fixed
Handle QBO API errors. When refresh token failed, server was just sending 500 error.
Now it responds with a meaningful error and an authURL to re-authenticate with QBO
Release
UpMeals_1.0.2022090602
Added
Page size selector in all paginated tables!
Show meal/ingredient/product/etc. name in browser tab's title
Also show meaningful titles for the rest of the pages
Changed
Put table actions inside a dropdown instead of showing lots of ugly buttons
Added icons to actions dropdowns in item lists
Update font-awesome lib to latest version (6.2.0)
Include common library module imports in the Shared module, and remove from other modules.
Fixed
Fixed a bug when saving a duplicate recipe or ingredient, API error complaining of invalid properties
Release
UpMeals_1.0.20220831
Added
"Used in" section for Sub-Recipes and Ingredients
Shows a list of parent recipes/meals, and amounts used
Fixed
Some recipe names disappearing from worklists
Release
UpMeals_1.0.20220830
Added
Added tool for deleting all duplicate recipe components from all recipes
Changed
Meal/Sub-recipe export to excel feature now applies the selected filters
Fixed
Fixed logic that excludes bulk items from daily prep
Fixed a bug where the "Total Meals" row got alphabetically sorted in the Execution and Packing worklists
Release
UpMeals_1.0.20220829
Added
Added basic responsive functionality to sidebar. Now the sidebar is hidden by default and toggleable on smaller screens.
Filter recipes by Active/Inactive
Changed
Active indicators more prominent for recipes/meals
New Sub-Recipes now active by default
Exclude Bulk recipe's components from Daily Prep and other worklists
Release
UpMeals_1.0.20220826
Added
Add sorting option to kitchen worklists
Options are "Auto" and "Alphabetical"
Default is "Auto" except for the Organizing Worklist
Compression middleware to backend: Reduced bandwidth up to 90% in some API requests
Changed
Update readme and /backend/.node-version file
Filter usage units, only include 'g' and 'kg' for weight, 'ml' and 'l' for volume, and 'ea' for eaches.
In some cases display all units of type 'each', like in item procurement
Angular: Separate Pages module into one module per page, including lazy loading
Improves load times and performance
Rename 'Instructions' to 'Re-heating Instructions', and 'Dietary Restrictions' to 'Dietary Preferences'
Create missing 'non-auditable' repositories for app data initialization
Users, Tenants, Units
Improved user activity detection for starting/stopping server polling in worklists
Fixed
Fix bad amount calculation in worklists when parent recipe has yield in 'eaches' and amount different than 1
Fix bug with Sunday production cutoff day calculation
Smoother user experience when checking multiple items in worklist in quick succession
Release
UpMeals_1.0.20220824
Fixed
Fixed a bug when deleting an ingredient from a recipe. Some unrelated recipe -> recipe relations could be deleted at the same time.
Release
UpMeals_1.0.20220816
Added
Excel export of recipes/meals
Kitchen Production Worklists now saved to backend with user & time metadata
Sync kitchen worklist status by polling the server for changes
Row showing grand total of meals for Execution and Packing lists
Changed
Changed the way "Parent recipes" are displayed in the worklists
Now shows a Used In: row which expands to show one parent recipe per sub-row
recipes include links to open them
Updated backend libraries to latest versions
Release
UpMeals_1.0.20220808
Changed
Modified print styles for worklists to reduce size and make more stuff fit into one page
In Packing worklist, separate customer totals and totals per location in different collapsible rows
Added
Add option to include methods in excel exports
Add Category to excel exports (except Organizing)
Option to export collapsed worklists to Excel (no components, only recipes/meals)
Release
UpMeals_1.0.20220802
Added
Checkboxes for individual items in worklists for print and excel exports
Add option to print only first-level recipes in worklists (no components)
Add the custom yield control to meals, it was previously only on sub-recipes
Changed
Allow multi-select in worklist type filter (catering and ready-to-eat)
Change logic of "include in organizing" checkboxes for Daily Prep recipes' components
Now checked means include in organizing, unchecked means exclude.
Fixed
Fix bug that prevented saving meal/recipe category in edit screen
Release
UpMeals_1.0.20220727
Added
Separate kitchen worklists for 'Ready to eat' and 'catering' meals
Par Stock field in Sub-Recipe bulk editor
Only for 'Bulk Worklist' Sub-Recipes
Release
UpMeals_1.0.20220726
Changed
Visual updates to kitchen worklists excel exports
Updates to Bulk Worklist excel export
Performance improvements for Kitchen Production Lists
Update Angular to v14
Update libs to latest versions supporting Angular 14
Fixed
Fix a bug with saving a new ingredient - "allergensMayContain property not found in model"
Fix logic error that sometimes resulted in null as quantity for worklist items
Release
UpMeals_1.0.20220721
Changed
In Kitchen Production Lists, Modify date ranges to exclude items to be delivered the same day of execution.
Exception being the orders from SPUD - later will be changed to a flexible configurable rule.
Release
UpMeals_1.0.20220718
Fixed
Fix a bug with costing calculations for new meals
When creating a new Meal, costing info would be blank and an error is shown in the bottom for missing yield unit
Fixed in UI and Backend
Release
UpMeals_1.0.20220716
Added
Added pricing tools to meal view costing tab
Display price per base unit (kg/L/ea) in recipe component list
New inter-dependent retail and wholesale price and food cost percentage fields in costing tab
Changed
Change permission required for GET /production-days to ViewKitchenProductionLists instead of EditGeneralSettings
Fixed
Adding the Meal Category filter to the Packing worklist
Remove Edit buttons and disable fields when the user doesn't have Edit permissions
Release
UpMeals_1.0.20220713
Added
Show date range info in kitchen worklists
Added permissions specifically for using the ingredient/recipe bulk editor
Restrict access to bulk editor in the UI
Changed
Change excel export for Bulk Worklist
Changes to Bulk Worklist logic and UI
Changed app title to "UpMeals App" instead of "Upmeals App"
Changed logic for the items displayed in Organizing Worklist.
Now the flag is "exclude" instead of "include", and all are included by default
Updates to Excel exports on Kitchen Worklists
Fixed
Fixed some logic for generating kitchen worklists
Fixed logic when saving tags that resulted in errors when saving same tag as "allergen" and "allergen may contain"
Clear cache after a bulk edit so the normal edit will sync afterwards
Fixed bug with recalculating ingredient amounts after adjusting yield in sub-recipes
Removed
Unused endpoints and permissions for audit log
Makes no sense to be able to edit, manually add, or delete audit log entries via REST
Release
UpMeals_1.0.20220706
Added
Added Organizing worklist logic
Added field for "include in organizing" for components of Daily Prep recipes
Removed
Removed all traces of "prep days" per recipe
Fixed
Fixed bug which accidentally deleted manual nutrition data for recipes when using bulk editor
Fixed layout issues with New Meal screen
Release
UpMeals_1.0.20220705
Added
Added a Bulk Editor for sub-recipes and meals
Added a Bulk Editor for ingredients
In the ingredients management screen, added a "Bulk Edit" button next to "New Ingredient"
Whole list of ingredients changes to a bulk edit interface
Added the ability to print meals and sub-recipes
Added recipe components to most worklists
show amount needed for a single worklist, and for the day's total
Added notice to worklists for when the cutoff time hasn't been reached
Added checkboxes to worklists. Store values in localStorage.
New "Packing" and "Organizing" worklists
Changed
Changes to the way data is handled in the printable worklists, use same datasource as the regular views
Use Production Days settings to calculate date ranges instead of a date picker
Changes to kitchen production endpoint, reduced payload and better data
Removed
Removed date picker from Kitchen Production Worklists
Removed individual dates in kitchen worklists, we only care about the total for that production day
Removed shelf life units dropdown from meals and recipes
Delete unnecessary logic from sanitizeItemToSave()
Hardcoded label "days" next to shelf life days
Fixed
Fixed some bugs with saving ingredients
Release
UpMeals_1.0.20220621
Added
Added ng2-dragula library to add sequence editor functionality
Add category columns
Drag items between columns
Filter meals by name or/and category
Added Production Sequence Editor component inside settings folder
Added endpoint for creating/updating meal sequences in recipe controller
A modal with basic audit logs details
A screen for viewing audit logs list
Audit logs for every business entity (such as ingredients, recipes, tags, all metadata and intermediate tables).
Saves date created/modified/deleted, user responsible, action name, a data snapshot and a diff.
Added keyboard shortcut for "save and close" (Ctrl+Shift+S or ⌘+Shift+S)
Added Production Days backend validation for duplicates
Added Productions Days CRUD logic
Added Production Days Frontend files
Create Producion Days component
Create Production Days model
Create Production Days API Service
Added Production Days Backend files
Create Production Days model
Create Production Days repository
Create Production Days controller
Added more unit tests to recipe-ingredients-list component
Added endpoint for switching tenants for current user
Added TsDoc to all methods that Mariano has been working on (frontend and backend), since he's a contractor
Adding CHANGELOG.md
Added an Angular Pipe that converts grams and milliliters to kg and L when amount is greater than 1000
Added a dropdown option to the Kitchen Worklist Print button, to include recipe methods (now excluded by default) in printable views
Added a helper method that combines a recipe's sub-recipes and ingredients into a single list sorted by position
Changed
Updates Recipes API Service, add new method to update the sequence numbers of meals
Updates Recipe Model
Add sequence primary and secondary type number in the backend model
Add sequence primary and secondary type number in the frontend model
View/Edit screens now open from the same screen you're on. No longer redirect to /meals, /sub-recipes, or /ingredients
Decouple generic management functionaliy from Management Components
Updates Excluded Product Categories component
Changes in the way you add a category
Category name required validation
Updates meals ingredient list, always show both manual and auto lists
Updates to /login and /me endpoints to return correct tenant info
Updates to user creation methods related to multi-tenancy
Updated Kitchen Worlkist printable layouts
Fixed
Fixed many bugs with My Account component
Fixed logic for loading products in Customer screen when the modal was opened in readonly mode initially
Fixed Allergens titles section in Meals/Sub-Recipes screen on edit mode
Release
UpMeals_1.0.20220526
Added
NFP PDF export
Added ability to search Meals, Sub-Recipes, and Ingredients by their ID as well as their name
Keyboard shortcuts for edit screens:
⌘+S (mac) or Ctrl+S (Windows) to save
⌘+E (Mac) or Ctrl+E (Windows) to edit
⌘+Z (Mac) or Ctrl+Z (Windows) to undo
Esc to close any modal window
Changed
Changed titles for Allergens section in Meals/Sub-Recipes screen
Set new meals as Active by default
New required fields for meals, sub-recipes, ingredients
Added intelligent cache to API queries in management/edit screens
Fixed
Fixed initial Nutrition Data for recipes, meals. It was saving an empty object instead of null, so nutrition data loaded as empty instead of auto-generated
Fixed a bug when saving "additional allergens" for sub-recipes/meals
Some code cleanup
Other various bug fixes
Fixed CI/CD Pipelines configuration to avoid errors with updated 3rd party libs - freeze their versions in package.json, and send package-lock.json to AWS CodeDeploy
Release
UpMeals_1.0.20220519
Added
Added Products feature, with ability to map products to clients
Endpoint to get API version info
Display API version in app's status bar
Nutrition Data re-calculates when recipe saved
Adding fields and properties to models: Recipe, Vendor
Backend additions for the ingredient list generator (future feature)
UI changes based on user permissions
Send role and permissions in the /login and /me endpoints
Restrictions on user create/edit/delete based on permissions
Roles and Permissions management
Auth changes for using roles and permissions in all endpoints
Added an observer for initializing ACL entities
Added setting for optionally requiring Vendor Code in vendors/procurements
Adding list of allergens from ingredients in recipe screens
Changed
Revamped the undo history system which fixes several bugs
Style updates
Added all requirements for CFIA compliant NFPs including %DV
Improve API error handling
Display correct error message when missing yield
Don't copy external ID when duplicating
Upgraded libraries in backend project
Adding relationType to tag intermediate tables
Fixed
Improvements in edit screens
Improved JSDOC in generic edit components
Fixed bug with saving tags and tag categories
Fixed adding "allergens" and "allergens may contain" in ingredients
Fixed permissions typo
Fixed AWS CI/CD config
Clean up linting errors
Removed
Removed all references to IngredientGroup model/repo/controllers, etc
Removed Vendor Abbreviation from everywhere
Release
UpMeals_1.0.20220405
Added
Graph for sales data in meals screen
Show errors related to Costing in recipe screens
Added error-checking for Nutrition Data inconsistencies
Display images in ImageUpload component
Login screen background and logo!
Added pagination totals to paginated tables
Current password and change password fields
Calculate Nutrition Data in GET endpoints for recipes
Display Ingredient cost in Ingredient view
Changed
Gray out rows without sales in Financial Report
Automatically assign ea unit and yield=1 to Meals
Display full name in users list
Moved nutrition data calculations off the recipe controller
Fixed
Breadcrumb links to meals now open in /meals route instead of /sub-recipes
Fix opacity of loading rows in all list tables
Fix loading indicator in all paginated tables
Prevent circular references in Costing and Labelling services
Release
UpMeals_1.0.20220324
Added
Show external IDs in meals, recipes, ingredients
Custom portion size in Nutrition Data View
Sales data tab in Meals screen
Show answers to questionnaire in Incoming Leads
Questionnaire component
On-the-fly cost calculations while editing recipe
Display an automatic "short ingredient list" for recipes
Costing endpoints and logic
Incoming lead / Customer functionality
Changed
Hide portion size for meals
Add custom yield to Costing tab
Fixed
Fix nutrition data search (Nutritionix) for sub-recipes
Fixes to Nutrition Data calculations and search
Style fixes for questionnaire
Validation errors for converting incoming lead to customer
Fixes to sales data, sort chronologically across years
Release
UpMeals_1.0.20220308
Changed
Enable Data Import tools in Production
Release
UpMeals_1.0.20220307
Added
Financial Reports Frontend services
Recipe Ingredients lists, sorted by weight
Nutrition Data Report screen
User Account
Sales Velocity endpoint
New fields for ingredients: Label Name, Vendor Components, Label Components
Questionnaire models and endpoints
Endpoint to fetch recipe nutrition data
Management of excluded QBO product categories
User management screens
Data import and integrity check tools
Nutrition data edit functionality
Calculate ingredient cost
Nutrition Data search functionality
Connection to Nutritionix
Customer and Customer Locations
"Discard changes" button to edit screens
Undo functionality
Filter ingredients by category
Exclude some invoices that are marked to not being included in app
Filter recipes by category
Basic filter logic in all management screens
Cron job for deleting expired QBO tokens
CI/CD configuration
Ability to duplicate recipes
Changed
New PR Templates
UI Changes to customer views
Deleting an ingredient now deletes all procurements too
Improve logic for how categories are fetched for ingredients and recipes
Updated readme
Separate routes for Meals and Sub-Recipes
Change sidebar link from Recipes to Sub-Recipes
filter getters for edit/manage screens are now async
Performance improvements for kitchen production print view
Remove meals from recipe ingredient lists dropdown
Date correction +1 day for actual invoice dates for KPL screens
Decouple orders sync from Kitchen Production controller
Store external API tokens in Mongo DB instead of in-memory
Clickable recipe names in Kitchen Production screens
Fixed
Bugs with nutrition data calculation
Undelete toast bug fix
404 bug when deleting recipe
Bug with search term in recipes/meals
Bugs with method that strips customer prefix off meal names from QBO
Fix bugs with ingredients presentations
Bugs with dropdowns in edit screens
Fixed pagination bugs related to filters
Fixed bugs with save button
Fixed various bugs in user account
Set customer status automatically for new customers