Developer updates

Changelog

Platform, developer, and integration-facing changes from Demi releases. This page is generated from Demi's source-controlled CHANGELOG.md.

Latest: 5.4.3

5.4.3

Added

  • 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 Square onboarding, retail dashboards, reports, sorting, responsive KPI cards, inventory reconciliation, and operator-facing insights.
  • 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.

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.

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.

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.

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.

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.

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.
  • Fixed Demi MCP host registration, consent, context naming and scope resolution, refresh compatibility, GlobalAdmin access, read-tool reliability, and durable private export processing.

Security

  • Removed OAuth codes, keys, OTPs, and tokens from analytics URL properties and cleaned authorization codes from the login URL after capture.

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.

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.

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.

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.

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.

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.

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.

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.
  • Fixed container kit component picker dropdown behavior.

5.3.2

Added

  • 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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).
  • Removed double slash in feedback API URL.
  • Fixed inventory setup validation false positives.
  • Fixed settings categories cache invalidation issues.
  • 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.

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.

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.

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.

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.

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.

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-tealbtn-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.

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 &lt;/&gt;/&#60;/&#x3c; 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/metricsCockpitController → 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 opportunisticallyVendorInfo 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 columnsIngredientInventoryConfig 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 ngAfterViewInit cdr.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 &lt;/&gt;/&#60;/&#x3c; 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 opportunisticallyVendorInfo 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 columnsIngredientInventoryConfig 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/downloadExcelpoApi.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.shstart.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-mysql existingSchema:'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_ENVproduction/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.
  • Docs + runbooks updated (docs/infrastructure/deployment-overview.md GCP readiness item #13 + AWS-prod schema-migration considerations; docs/infrastructure/gcp-cutover-runbook.md P3d post-cutover index + addressId parity checks including mandatory follow-on-redeploy re-verification).
  • 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 methodsgetPendingTasks() 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 --wait before gcloud 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 listsubmit() 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" clicksstartReceiving() 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 digitswhite-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 prefixroundForDisplay() 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 removedstyle="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 tokensp-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 filterum-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 stylestext-primary added to "Receive all" link button; back-nav buttons corrected to text-primary text-nowrap.
  • Start count spinnermr-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-deleteDELETE /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 actionsprintOrder() 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 nullonUnitChange() 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 viewerwin.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 #vendorTpl ng-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 defaultBatchExportService.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 receiptseditQty 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 amberbadge-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 jobhubspot-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.19.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 scriptscripts/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-opGenericManagementService.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 dialogdefaultAlertOptions 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 definedDemiSalesOsCard.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 loadpurchase-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 dialogspreConfirm, 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 forkJoin next 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 stagingPurchaseOrderService.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 &lt;/&gt;/&#60;/&#x3c; 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/metricsCockpitController → 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 opportunisticallyVendorInfo 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 columnsIngredientInventoryConfig 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 ngAfterViewInit cdr.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 &lt;/&gt;/&#60;/&#x3c; 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 opportunisticallyVendorInfo 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 columnsIngredientInventoryConfig 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/downloadExcelpoApi.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.shstart.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-mysql existingSchema:'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_ENVproduction/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.
  • Docs + runbooks updated (docs/infrastructure/deployment-overview.md GCP readiness item #13 + AWS-prod schema-migration considerations; docs/infrastructure/gcp-cutover-runbook.md P3d post-cutover index + addressId parity checks including mandatory follow-on-redeploy re-verification).
  • 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 methodsgetPendingTasks() 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 --wait before gcloud 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 listsubmit() 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" clicksstartReceiving() 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 digitswhite-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 prefixroundForDisplay() 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 removedstyle="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 tokensp-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 filterum-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 stylestext-primary added to "Receive all" link button; back-nav buttons corrected to text-primary text-nowrap.
  • Start count spinnermr-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-deleteDELETE /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 actionsprintOrder() 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 nullonUnitChange() 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 viewerwin.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 #vendorTpl ng-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 defaultBatchExportService.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 receiptseditQty 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 amberbadge-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 jobhubspot-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.19.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 scriptscripts/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-opGenericManagementService.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 dialogdefaultAlertOptions 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 definedDemiSalesOsCard.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 loadpurchase-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 dialogspreConfirm, 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 forkJoin next 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 stagingPurchaseOrderService.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 extractedPHYSICAL_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-xlmb-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 authAiGatewayService 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 scopingapplyCustomTenancyContext() 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 tripsAiStreamService 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 leakProductInsightsController.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 corruptiongetInsightsStream 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 OOMAiUsageLogRepository.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 bugDECIMAL 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 contextProductInsightsService.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 bypassAiSavingsReportService.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 contextacceptInsight 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 crashuserId 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 isolationbuildBulkCaseUpdate() 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
  • Tenant repo cache bypass — added bypassCache for batch context lookups
  • Subscription flow review & delight — complete redesign of all 4 subscription lifecycle flows:
  • Inactive subscription page auto-detects reason (trial expired / payment failed / voluntarily cancelled) with tailored messaging, role-gated, inline pricing cards
  • 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
  • FEATURE_TIER_MAP constant — centralized feature-to-tier display config for consistent upgrade messaging
  • 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 supportum-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 productionshowInventoryModule flipped to true, gated by subscription tier instead of environment flag
  • production: true in environment.prod.ts — enables Angular prod mode (was incorrectly false)
  • 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 onlysendToOrdersInbox() 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)
  • Cutover runbook: post-cutover cleanup — added bastion VM teardown, AWS SG cleanup, Datastream backfill verification
  • Cutover runbook: Cloud SQL snapshot — added explicit gcloud sql backups create command and verification step
  • 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
  • Deploy workflows updated with --cpu-boost, Cloud SQL Auth Proxy sidecar, --min-instances=5 (production)
  • 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
  • Admin notification settings page — Owner/Admin can toggle 20 email notification types across 6 sections (Orders, Customers, Operations, Integrations, Billing, Account)
  • 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
  • 58 unit tests — NotificationPreferencesService (37), NotificationService (14), SendEmailService (4), covering sendEmailOnly, preference filtering, tenantId passthrough, per-user personalization
  • 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 personalizationsendEmails() now merges user_first_name from each User object into template data, ensuring each recipient sees their own name
  • Org defaults tenant isolation in webhooksgetOrgDefaults(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()
  • Login controller TS error — fixed pre-existing activatedUserTenant possibly null TypeScript strict check
  • Meeting preference documentation — documented fail-open ?? true as intentional design decision
  • 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.
  • Reorder Recommendations UX — Collapse all/Expand all toggle (KPL pattern), vendor dismiss button (composite vendorId:ingredientId keys), "Start PO" rename, deficit column removed for cleaner V1
  • 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 enrichmentgetReorderRecommendations() now returns vendorCode and purchaseUnitLabel per item, includes procurement unit relations, and excludes ingredients already in open (draft/submitted) POs
  • Paginated table row click brokencellAction() 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.
  • Paginated table cellAction null guard — replaced fragile !idx && typeof idx !== 'number' with explicit idx == null
  • 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 warningremovalConfirmed 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 emptyloadVendors() 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 escapingescapeHtml helpers now escape single quotes ('&#39;) 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 (&uarr;/&darr;) with unicode characters in Angular interpolation
  • Emergency export column names — fixed createdAtcreated_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_PASSWORDTARGET_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)
  • Bulk onboard async path returns jobId immediately via setImmediate (prevents HTTP timeout)
  • Offboard verifies sub-tenant belongs to caller's tenant before mutation (tenant isolation)
  • MongoDB idempotency guard catches E11000 duplicate key errors (prevents race condition duplicates)
  • Formula injection stripping handles all leading =+-@ characters (not just first)
  • Firebase Admin SDK initialization tolerates concurrent "already exists" errors
  • 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
  • Wizard spec error mock shape fixed to correctly trigger fallback error message
  • 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
  • Sub-tenant datasource connection failures now properly disconnect partially-initialized datasources (resource leak fix)
  • 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
  • BigQuery CDC pipeline — Datastream replicates Cloud SQL to BigQuery, enterprise analytics views (revenue, food cost, benchmarks, vendor comparison)
  • 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 Cloud Run deployment infrastructure — Dockerfiles, nginx config, CI/CD workflow, GCP staging environment
  • reCAPTCHA Enterprise upgrade — backend + frontend migration from reCAPTCHA v2 to Enterprise API with v2 fallback for AWS
  • Cloud Run-compatible instance identity — GCP metadata + UUID fallback for leader election (replaces AWS IMDS-only)
  • Post-deploy controller with HMAC timing-safe auth for CI/CD-triggered Typesense reindex and dashboard warm markers
  • Storage provider abstraction — S3/GCS dual-mode config with STORAGE_PROVIDER env var and HMAC key support
  • READ_ONLY_MODE env var for GCP parallel stability period — suppresses crons, leadership claims, and schema migration
  • GCS URL acceptance in recipe import file validator (alongside existing S3 support)
  • 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: b2b-reminder — add 28-day deliveryDate filter (was unbounded), O(N) Map grouping (was O(N²) filter loop), pLimit(5) on email sends
  • Cron audit: clean-progress-tasks — batch deleteAll replaces N+1 per-task deletes, limit:500 safety cap on both queries with backlog warning
  • Cron audit: meetings-rolling-pre-auth — pLimit(5) concurrency + Promise.allSettled + 30s per-meeting timeout with clearTimeout, await email sends
  • 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

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)
  • Inventory sidebar navigation gated behind ViewIngredientInventory permission
  • 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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)

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

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

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

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

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

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

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
  • MongoDB materialized views (recipe_full, order_summary, product_full) eliminating expensive aggregation joins
  • 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

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

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

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)

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

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

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

4.3.0

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

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

4.0.2

Fixed

  • Daily Prep worklist now displays correct quantities when stock tracking is disabled, matching the Print view values

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

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

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

3.2.1

Fixed

  • MC: Meeting wizard now correctly advances to the guest invitation step instead of exiting edit mode when clicking "Next"

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

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

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

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

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")

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

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

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

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

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

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.

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)

2.8.1

Fixed

  • Bulk worklist orders modal now correctly shows only current period orders (orders for execution days after the bulk production day)

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.

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.

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

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)

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

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.

2.4.2

Fixed

  • Printing workflows grouped by Customer or Component wasn't working, printed an empty page

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

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

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

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

2.2.1

Fixed

  • Needed vs To Make calculations for the Daily Prep workflow were not showing accurate values

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

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.

2.0.1

Fixed

  • Fixed issue where QBO status was not being updated correctly when the user was redirected to the QBO authentication page

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

---

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

1.4.1

Added

  • Restored Modern Catering cart toggle functionality

Changed

  • Changed font across all Modern Catering UI to the standard Demi font

1.4.0

Changed

  • Switch to new Demi stripe account

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

1.3.8

Added

  • Printable product spec sheet accessible from the recipe edit page

Changed

  • Redesigned printable recipe view with improved layout and styling

1.3.7

Fixed

  • Fixed image sizing for image display component and order edit

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

1.3.5

Fixed

  • Fixed server CORS configuration issues
  • Tighter security for admin user credentials

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

1.3.3

Fixed

  • Fixed display bug in printable workflows which wasn't adding spacing between columns

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

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

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

1.2.3

Fixed

  • Fixed Modern Catering layout bug

1.2.2

Fixed

  • Fixed status drodpown selector in product edit component

1.2.1

Fixed

  • Fixed issue where SaaS Admin roles were not able to see the "Customers" dropdown in the "Users" management screen

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

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
  • CAD prices displayed without CAD data; annual-to-monthly math; inconsistent subscription currency.

1.0.0

Added

  • 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

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

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)

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

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

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

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

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

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

UpMeals_8.5.3

Fixed

  • Fixed issue where file uploads were not working correctly

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

UpMeals_8.5.1

Fixed

  • Added logic to clean up invalid permissions from all roles in the system

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

UpMeals_8.4.1

Fixed

  • Removed Organizing, Production and Packing from Workflow options in sub-recipe management filters and sub-recipe edit form

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

UpMeals_8.3.1

Fixed

  • Fixed issue where recipe costing was empty after recent changes to the database schema

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

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

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

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

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

UpMeals_8.0.3

Fixed

  • Sub-Tenant wasn't always being enforced for some users, leading to wrong data in their Products list

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

UpMeals_8.0.1

Fixed

  • Fixed Meals<>Products migration script to account for multiple products pointing to same meal

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
  • Onboarding visual refinement: transition animations, step indicators, collapsible address input component, back buttons
  • 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

UpMeals_7.25.2

Changed

  • Intercept errors when app is outdated and show the "New version available" dialog

UpMeals_7.25.1

Fixed

  • Fixed issue where the sub-tenant context was not automatically setting the default available sub-tenant

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

UpMeals_7.24.4

Fixed

  • Fixed issue where ingredients were not being fetched correctly in kitchen production

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

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

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

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

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

UpMeals_7.23.1

Changed

  • Move background processing of created ingredients/recipes to the backend

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

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

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

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

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

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

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

UpMeals_7.19.1

Fixed

  • Fixed ingredient label compliance sync cron job
  • Isolate tenant database scope
  • Implement forceSync parameter

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

UpMeals_7.18.3

Fixed

  • Fixed translations not being saved for recipes

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

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

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

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

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.

UpMeals_7.16.8

Fixed

  • Fixed Stripe integration not dynamically loading tenant's publishable key

UpMeals_7.16.7

Fixed

  • Prevent scroll wheel from changing number input values throughout the app

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

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

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

UpMeals_7.16.3

Fixed

  • Fixed issue where swapping a recipe component would not update the amount/unit input

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

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

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
  • Automatically fetches replacement metadata (cost, nutrition)
  • Sentry triage github action

Changed

  • Make Shelf Life not required in the recipe edit screen

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

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

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)

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

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

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

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

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

UpMeals_7.9.1

Fixed

  • Fixed issue where MC shop for guest invitees was getting stuck

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

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

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

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

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

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.

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

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

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

UpMeals_7.5.3

Fixed

  • Fixed error when saving meals related to invalid nutrition data

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

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

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

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

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

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

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

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

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

UpMeals_6.11.20250503

Fixed

  • Improve QBO product sync error handling and fallback strategy

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.

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

UpMeals_6.11.20250315

Fixed

  • Fix bug that miscalculated next production day for a given date.

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

UpMeals_6.11.2

Fixed

  • Fixed no totals or tax amounts on some order email notifications

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

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

UpMeals_6.10.20250224

Changed

  • Improve cron job admin panel

Fixed

  • Ignore par stock calculations for pre-execution worklists

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

UpMeals_6.9.20250220

Added

  • Added task progress tracking for long running tasks in backend/frontend

Changed

  • Improved financial report performance
  • Fixed ingredient usage report's performance, added progress tracking

Fixed

  • Fixed totals in Bulk worklist and excel export
  • Fix secondary labels for meeting guests who joined via link

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

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

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

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

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

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.

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

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)

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

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

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

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

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

UpMeals_6.0.3

Fixed

  • Fixed invoice sync issues for meal plans

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

UpMeals_6.0.20241101

Fixed

  • Errors with DST date calculations in some ordering screens

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.

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

UpMeals_5.11.0

Changed

  • Changes to UI and backend to eliminate long loading times when placing meal plan orders and deleting team members

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

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

UpMeals_5.8.20241017

Fixed

  • App crashes sometimes when creating new B2B order

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

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

UpMeals_5.7.20240924

Fixed

  • DB and logic changes to reduce database usage related to historic worklists snapshots

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

UpMeals_5.6.20240920

Fixed

  • Missing Meal Plan order details for meal plans with only Saturday enabled

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

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.

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.

UpMeals_5.3.20240816

Fixed

  • Hotfix for logging issues for smartvending forecasting orders.

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

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

UpMeals_5.2.20240808

Fixed

  • Internal server error sometimes triggered when editing users

UpMeals_5.2.20240803

Fixed

  • Team Members having issues placing meal plan orders

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

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

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

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

UpMeals_4.20.20240714

Fixed

  • MC users export wasn't correctly filtering MC customers

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

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.

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.

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.

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

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

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

UpMeals_4.16.1

Fixed

  • Fixed time ranges in packing summary report, change export format from CSV to XLSX

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

UpMeals_4.15.20240629

Changed

  • Assign MC shared lineup to new customers from MC Onboarding flow

UpMeals_4.15.20240619

Fixed

  • Fix code that looks up an address by postal code using google's API

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

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

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

UpMeals_4.14.0

Added

  • Added new meal plan animations

Fixed

  • Fix promo code and tax calculations in MC

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

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

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

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.

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

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

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

UpMeals_4.13.20240522

Added

  • Endpoint to fetch product data for SmartVending order forecast

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

UpMeals_4.13.20240511

Fixed

  • Fixed opening "Current" meal plan order in Meal Plans Dashboard

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

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

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

UpMeals_4.13.20240508

Fixed

  • Removed some debug code that was causing QBO invoices to fail syncing

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

UpMeals_4.12.20240503

Added

  • Added a "copy invite link" button to invitation meeting details for each invitee

UpMeals_4.12.20240427

Fixed

  • Fixed meal plan week number calculations
  • Fixed horizontal scrollbar issue in modals

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.

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

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

UpMeals_4.12.20240328

Fixed

  • Fixed a bug where MC shop wouldn't load for meeting guests

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.

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.

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

UpMeals_4.11.20240311

Changed

  • Show under-minimum delivery fees in the Meal Plan Dashboard

Fixed

  • Restore access to /mc for non-authenticated users

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

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

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

UpMeals_4.8.2024012902

Fixed

  • Prevent sending duplicate invite emails to users that have already confirmed their picks

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

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.

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

UpMeals_4.7.3

Added

  • Added CSV importer for smrt1 product ids

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

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.

UpMeals_4.7.1

Added

  • Added container management and created association between container and meals

UpMeals_4.7.20240118

Fixed

  • Hotfix for not sending unnessary 'meeting updated' email to all invitees.

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.

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

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

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

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

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)

UpMeals_4.4.20231206

Fixed

  • Could not save questionnaires, the server returned an error

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

UpMeals_4.4.20231201

Fixed

  • Hotfix for enabling email while adding user/team-member

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

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

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

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

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

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

UpMeals_4.2.20231115

Fixed

  • Bug that caused payment method selector to some times be disabled for team members of companies with credit terms

UpMeals_4.2.20231113

Fixed

  • Hotfix for showing product picks in past cutoff meetings

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

UpMeals_4.2.20231109

Fixed

  • Logic for new b2b order email recipients - was incorrectly sending the notifications to a test email account

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

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

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.

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.
  • Added sendgrid dynamic templates for welcome emails, meeting creation, meeting declined scenarios.

Fixed

  • Added validation to stop user during invitee meeting creation if no attendees are invited.
  • Fix refund error when cancelling an invite mode meeting
  • Fixed email subject lines and email template issues for meal plan transactional emails.
  • Fixed shop sidebar independently scrollable

UpMeals_4.2.6

Added

  • Allowed adding of list of emails both by line break and comma separated in meeting invite by email text area.

Changed

  • Improved performance of Meal Lineup in the shop
  • Implemented cache for SVG icons to drastically reduce HTTP requests
  • Use smaller version of product images in the shop
  • Skip payment method step if customer doesn't want to subsidize meal plan nor he wishes to pay for order minimum fee.
  • 15 min inerval time picker in the edit meeting modal

Fixed

  • Fixed email sending only to creator of meal plan, should go to invitees.
  • Fixed code to send confirmation emails to creator of meal plan or meeting, even when they're not an invitee.
  • Adjusted image width in meeting invite email and fixed some design bugs in meeting confirmation email template.

UpMeals_4.2.5

Added

  • Added new email template for transactional emails

Fixed

  • Removed blur effect on the shop when offcanvas cart is opened + removed click trigger outside of cart to close it

UpMeals_4.2.4

Added

  • Added new welcome email template for modern catering onboarded users
  • Added logic to send confirmation emails to meeting, meal plan creator even though he's not invited
  • Added Declined and Past status indicators for meetings list.

Changed

  • Separated meal plan creation out into a multi-step process

Fixed

  • Fixed bug where new users added from app dashboard not receiving welcome emails
  • Several minor UI bug fixes

UpMeals_4.2.3

Changed

  • Change "Discounts" for "Company subsidy" in checkout screen for meal plans

Fixed

  • Fixed role dropdown showing when editing own user.
  • "Shopping cart missing metadata" error fixed
  • Fixed a bug that sometimes prevented meeting checkout after user logged out and logged back in
  • Fixed missing shopping cart items when editing meal plan picks if more than 1 item of same kind was purchased

UpMeals_4.2.2

Added

  • The team member status indicator in Team Member list has been changed to one that better indicates the status of each of the team members.
  • Added simplified meeting view for team members and meeting organizer.
  • Logic to separate welcome emails based on user type.
  • Add new customer location showing up for team members and meeting organizers and location admins.

Fixed

  • Validating that the SVG path is valid before attempting to draw it.
  • Fixed return url for meal plan and added select all in meal plan invite
  • Fixed customer dropdown appearing for all roles, it should only show up for few specific roles.
  • Fixed search bar not working in the meal plans.

UpMeals_4.2.1

Added

  • Meal Plans:
  • Settings screen for Meal Plans cutoff time
  • Validations for Meal Plan week number and cutoff time in shop and checkout
  • Cron job for charging meal plan orders at the cutoff time
  • Ask for payment method in Meal Plan create/edit screen
  • Added password eye icon to view password
  • Disable location selection modal for users with only one location available
  • Added Access Control restrictions for Meal Plans
  • Fixed location cache not clearing.
  • Disabled email editing and fixed persistent email already exists bug

Fixed

  • Fixed next button not working after going back and forth in meeting creation modal.
  • Meeting picks now include the tax when calculating limit.

UpMeals_4.2.0

Fixed

  • Fixed going to /mc showing unauthorized error.
  • Fixed bug where newly created locations are always B2B.
  • Fixed upcoming meetings list showing past meetings in dashboard.
  • Fixed newly created meetings not showing up in dashboard.
  • Fixed attendees count not showing in upcoming meetings list item
  • Prevent user from choosing a date which is post cutoff time while creating/updating meeting
  • Fixed payment methods not showing for team member
  • Added black color to dietary filter icon in mobile shop
  • Fixed new locations in mc being created as b2b
  • Fixed team member not able view meetings he is invited to.
  • Fixed number input in shop accepting decimal values.
  • Fixed API calls for Meal Timing tags.
  • Improved the icon component to handle cases when the file doesn't exist.

Added

  • Meal Plans management screens
  • Email notification and link to meal plan when invited
  • Email notification with details about meal plan picks, and link to edit
  • cart functionality to be able to select meals for a meal plan, day of week selector
  • Subsidy amount calculations are applied to meal plan totals
  • Refunds and secondary charges when user edits meal plan picks
  • Added clickable cursor pointer for calendar and clock icons in meeting detail view.
  • Added Info message in meeting detail view in the place of empty buttons
  • Prevent user from creating a duplicate meeting with same meeting name,date,time,location.

UpMeals_4.1.6

Added

  • Added hourly cron job for placing meeting orders that are past cutoff
  • Added the decline meeting functionality.

Fixed

  • Fixed bugs related to product picks and swap mode
  • Fixed bug with clearing cart only clearing half of the items.

UpMeals_4.1.5

Changed

  • The dietaryBenefits property now contains an array of the benefits that the meal or recipe has

Added

  • Sorting options in Orders list: Sort by delivery date, order ID, both ASC and DESC
  • Add "Subcategory" tag category
  • Payment method selection when creating an invitee meeting
  • Pre-Auth is placed on the payment method for max budget
  • If attendee number or budget changes and it's greater than previous one, pre-auth is released and new one is placed
  • Add MealPlan Controller Model Repository on Backend
  • Add "Meal Timing" links to shop sidebar
  • Send emails with link to view or change automatic product picks for invitees
  • UI for letting team members swap their picks, or change selection completely

Fixed

  • Bugs related to shopping cart/checkout
  • Bugs related to customer location selection

UpMeals_4.1.3

Added

  • Add Invitees to meetings
  • Automatic suggestions for meeting invitees

Changed

  • Toast redesign.
  • The Delete toast is changed from 'danger' to 'success'.
  • Show only b2b orders in b2b orders screen and vice verca for MC interfaces
  • Allow customers to place orders in MC with company credit terms
  • Only certain users will have permission to do so

UpMeals_4.1.20231012

Fixed

  • Add new tags to Ingredient/Recipe Bulk editors

Changed

  • Rename "Blueprint Group" to "Meal Generator Tags"

UpMeals_4.1.2

Fixed

  • Allow either uppercase or lowercase emails for login (<TestUser@UpMeals.ca> is the same as <testuser@upmeals.ca>)
  • When token expired, users were landing on a white screen with an error and unable to do anything

UpMeals_4.1.1

Fixed

  • Automatically log out users if there is an issue with credentials and a new token needs to be created
  • Fix bugs with cutoff times for orders

UpMeals_4.1.0

Added

  • Promo codes system
  • Dietary Profile preferences for Modern Catering users
  • Added dashboard widgets
  • Send QBO invoice logic added
  • Show "placed by" in meeting view
  • Send email to Sales reps when an order is modified
  • Added search input for name, lastname, email, and username
  • Added categories Proteins and Meal Timing in Tags
  • Added text and button when the shopping cart is empty on /checkout page
  • Added meeting edit, cancel permissions

UpMeals_4.0.11

Fixed

  • Fix breaking change that prevented editing orders

UpMeals_4.0.10

Added

  • 'Users' tab in Edit/View Customer screen

Fixed

  • Validate the order cutoff time when saving to database
  • Fix errors when cancelling orders from the actions menu in order list

UpMeals_4.0.9

Added

  • Allow to edit meeting items
  • Modify existing orders, sync with QBO
  • Issue refunds if applicable
  • Charge outstanding balances if applicable
  • Added teammember management for Modern Catering

UpMeals_4.0.8

Added

  • Tool for finding products/meals name discrepancies

Fixed

  • Ingredient and Recipe duplicates would share same NutritionData object, leading to inaccurate nutrition data in recipes and meals.
  • Fixed a bug where kitchen production worklists stayed in loading state permanently after a browser refresh
  • Fixed a bug in all management screens where filters and pagination would get lost upon page refresh

UpMeals_4.0.7

Added

  • Added Email notifications for MC

Changed

  • Changed wording from "Shipping" to "Delivery"

Fixed

  • Fix calculations for earliest possible delivery dates for same-day delivery clients

UpMeals_4.0.6

Fixed

  • Earliest possible Delivery date was not being calculated properly for same-day delivery clients

UpMeals_4.0.5

Fixed

  • Only sync customers to QBO if both the location OBO ID and the customer QBO ID are missing

UpMeals_4.0.4

Added

  • Create QBO Customers when finishing Onboarding flow or when placing orders
  • Added loading for category and dietary filters

Changed

  • Restrict Order/Meeting date to be the earliest possible delivery date or later

Fixed

  • Sometimes shopping cart didn't update shipping cost when first creating a meeting
  • Display shipping lines in orders

UpMeals_4.0.3

Fixed

  • (Hotfix) Fix initialization bug that prevented guest users from visiting /mc/join and /mc/shop

UpMeals_4.0.2

Added

  • Delivery amount calculations
  • Order amount validations and messages
  • Fixed number input
  • Added trash icon to number input component
  • Fixed cart closing updating number input
  • Added name and logo in headerbar

Fixed

  • Fixes for 7/17/2023 demo
  • First-time onboarded users would get an error when checking out an order for the first time

UpMeals_4.0.1

Changed

  • Some fixes for an MC release

UpMeals_4.0.0

Added

  • Modern Catering Dashboard, including these features:
  • Whole new interface for MC customers
  • Full store front for showcasing the meal lineup for MC customers
  • Shopping cart
  • Meeting creation and management
  • Full checkout flow
  • Payments integration via Stripe
  • Can manage personal credit cards (users) and company credit cards (Customer Locations)
  • User profile management
  • Team members list screen
  • Company info management

UpMeals_3.0.21

Added

  • "Resend Invite" action in users management
  • Show 'Last Login' in users list

Fixed

  • Company Location Users aren't showing
  • Clear customer/location on user edit screen when changing role or customer

UpMeals_3.0.20

Added

  • Added 'Duplicate' action for Roles management

Changed

  • Sort customer locations alphabetically

Fixed

  • Made email field for Customer Locations optional

UpMeals_3.0.19

Added

  • Email validation to Customers/Locations (allow comma-separated list of emails)

Fixed

  • Fix logic to anyone with the ViewCustomerGeneral permission to view customers
  • Prevent sending invalid emails to QBO
  • Fix product thumbnail sizing issue

UpMeals_3.0.18

Fixed

  • Visual fixes for image upload component

UpMeals_3.0.17

Fixed

  • Visual fixes for Order Details
  • Update default product image
  • remove decimals from calories
  • Include all location/customer emails in QBO invoices when creating orders
  • Audit Log fixes to UI and logic
  • Fix password validation error in edit user screen

UpMeals_3.0.16

Fixed

  • Some Customer Location Admins were seeing all the customer locations and not only their assigned ones when placing an order.

(the order failed but they were still able to see them. Not they aren't.)

UpMeals_3.0.15

Added

  • More user-friendly and informative errors for B2B customers when there was an error pushing an order to QBO

Fixed

  • Fix authorization error for Customer Location Admins

UpMeals_3.0.14

Added

  • Modern catering Onboarding
  • Redirect MC admins to MC interface upon login
  • Allow logging in with email address and password instead of only username and password

Fixed

  • Copy Previous Order sometimes copied an order for the same customer but a different location

UpMeals_3.0.13

Fixed

  • QBO invoices were being created when cancelling an order with no QBO ID

UpMeals_3.0.12

Fixed

  • Customer Location lost when syncing from QBO for some orders
  • Could not edit orders without customer location
  • Clean email addresses before sending to QBO to prevent errors
  • Customers with $0 min order and all items at $0 cost wouldn't let user place order
  • Customer name incorrect in billing address (in QBO) for some customers
  • Fixed logic for prev and next orders, and for Copy Previous Order

UpMeals_3.0.11

Changed

  • Reverting the verified meals filter in Order placing screen

UpMeals_3.0.10

Fixed

  • Customer/Location admins couldn't place orders due to authorization errors

UpMeals_3.0.9

Added

  • New ACL permission for ViewGeneralSettings

Changed

  • Filter out unverified meals from Order screen

Fixed

  • Filter out unauthorized customers/locations for customer/location admins in order screen

UpMeals_3.0.8

Added

  • Partial QBO Customers importer that just syncs the new and deleted ones and doesn't modify the existing ones.

UpMeals_3.0.7

Added

  • Added placeholder for image and calendar icon for delivery date.

Changed

  • Changes to Audit Log to reduce storage
  • Allow showing/editing QBO ID in customer/location UI
  • Append customer name to billing address in QBO invoices
  • Append customer location name to shipping address in QBO invoices
  • Save customer/location email addresses in QBO invoices

Fixed

  • Fixed order placement bugs
  • Fixed timezone-related errors with order placement
  • False infinite recursion error when saving some ingredients

UpMeals_3.0.6

Fixed

  • Bug that caused customer importer to fail
  • Bug saving new customer locations

UpMeals_3.0.5

Fixed

  • Fixed bug that sometimes allowed users to edit past orders

UpMeals_3.0.4

Added

  • Added group by category for product line up
  • Added recipe category filter for order

Changed

  • Send new order email notifications only to dev email in all environments except Production
  • For each order, store the name, email, username, and roleType of the user that placed it

Fixed

  • Fix errors for prev/next order buttons, and for "copy last order" function.

UpMeals_3.0.3

Fixed

  • Hotfix for orders screen crashing

UpMeals_3.0.2

Added

  • Added new order notification
  • Links to products and customers in order details screen
  • GET /orders/{id} endpoint now sends the 'createdBy' and 'createdByUser' properties
  • limit price to 2 decimals in order detail
  • Prevent users from changing their own email/username

Fixed

  • Phone number validation error
  • 'In Production' badge not showing
  • Remove requirement of at least $250 for min order amount
  • style updates and field validation fixes

UpMeals_3.0.1

Fixed

  • Order status is now calculated in the frontend reducing the API calls
  • Correct the cutoff times in Orders screen if the user is in a different timezone than the kitchen team
  • Allow any string to be saved as customer email - accept comma-separated email lists that come from QBO

UpMeals_3.0.0

Added

  • New B2B Ordering Platform
  • Added Orders management dashboard
  • Backend for placing orders, with access control
  • UI for placing orders
  • Added order confirm view
  • Create and update QBO invoices from orders
  • Reorder last order feature
  • Import orders from QBO invoices
  • Customer location management
  • Warning modal while deactivating customer location
  • New user types: Customer Admins and Customer Location admins
  • Added permissions for Customer Admin and Customer Location Admin
  • Users can be linked to a customer/customer location.
  • Login flow for users associated for multiple customers/locations
  • Send welcome email to new users
  • Blocking Login access for inactive customer locations users
  • Customers/Locations sync from QBO
  • Custom checkbox styles

Changed

  • Don't allow users with the same email

Fixed

  • Ingredient list was showing an old version when a meal was duplicated and then the ingredient list was set to auto

UpMeals_2.4.0

Added

  • Detailed meal lineup view for customers
  • Meal lineup export in Excel format
  • Added forgot password functionality for b2b

Fixed

  • Fix bug that prevented uploading images

UpMeals_2.3.2

Fixed

  • Fixed a bug that crashed the app when seeing some customers' product lineup
  • Fixed a potential bug if the cutoff time was set at 12:00 noon
  • Kitchen production screens sometimes didn't let the user select the next production day in the date picker
  • Some general code celanup

UpMeals_2.3.1

Added

  • Endpoint that, for a given delivery date, gets the last possible date/time to place an order that will be included in

the closest execution day prior to that delivery date.

  • Add "Ingredient Category" column to Ingredient Usage Report

UpMeals_2.3.0

Added

  • Endpoint for getting product data, to be used in Catering app and other apps that require products sync
  • Screen to create and activate API keys to be used by external apps

UpMeals_2.2.4

Changed

  • Add the Label Data view back to Meals screen

UpMeals_2.2.3

Added

  • Meal links from product screen

Fixed

  • Added back the tags to the label data view
  • Make verify button clickable in label data view

UpMeals_2.2.2

Added

  • Ingredient Usage Report export, from the Financial Reports screen

Changed

  • Moved label data tab from meals to products

UpMeals_2.2.1

Added

  • Added code to send notification for a meal on shelf life change
  • Added indicator for auto/manual total weight on meals
  • Added blueprint group tag

Fixed

  • Error when modifying customer's same-day delivery days
  • Fix wrong ingredient list order when a sub-recipe has yield in eaches different to 1
  • Fixed some calculations with historic production days

UpMeals_2.2.0

Fixed

  • Fixed Wrong URL copied on recipe path

Added

  • Added notification system
  • Recipe Stat Widget for main Dashboard
  • View past and future production days in kitchen worklists
  • Configure same-day deliveries per customer/day-of-week
  • Added Notes for ingredients
  • Added create meal revision option to not duplicate UPC for meals

Changed

  • Removed requirements from customer form
  • Removed form validaiton flag for save button

UpMeals_2.1.1

Added

  • Cleared used in arrays when duplicating recipes
  • Added filter to show only active used in meals or subrecipes

Fixed

  • Translated content was being shared among recipe/ingredient duplicates when using the 'duplicate' function. Fixed.

UpMeals_2.1.0

Added

  • Added validation to only allow unique recipe names
  • Added tooltip for processed amount
  • Screen to add Sales notes to Production worklists
  • Include Sales Notes in Worklist screens and exports
  • Added questionnaire dynamic links and data visualization.
  • Added the Lead questionnaire management

Fixed

  • Prevent circular dependencies in recipes
  • Fix an app crash caused by deleted vendors, affecting search function

UpMeals_2.0.0

Added

  • Number of portions field on meals
  • NFPs now show adjusted nutrition data when a meal has multiple portions
  • Global settings for adding/removing languages
  • Multi language support for label-related fields and metadata
  • Importer for QBO products and customers
  • "Sold As" section for Meals, in Costing tab
  • Ctrl or Cmd + P now prints the recipe/meal when the view/edit modal is open
  • Added "wholesale" or "retail" sales type to customers
  • Automatically calculate price for products
  • Automatically suggest a sale price to product/customer relations
  • Errors modal in kitchen production screens now closes with ESC key
  • Add a link to each QBO invoice in the Invoices modal in kitchen production screens

Fixed

  • Some errors in Kitchen Production lists were not being shown in the actual invoice data. Fixed.
  • Fixed styling for printed recipe/meal
  • Fix 'delivery date' for invoices in kitchen production screens. It was showing in the wrong timezone.

UpMeals_1.0.20221201

Added

  • Add-ons (Cutlery) management component

Fixed

  • Fixed broken vegan icon in Label Data View
  • Fixed no weight shown on NFP for recipes with auto-calculated weight

UpMeals_1.0.2022111502

Added

  • Option to auto-calculate weight for Sub-Recipes, not only Meals
  • Tool to delete all empty nutrition data from the database (left over from previous versions of the app)

Fixed

  • Bug with auto weight calculation: Sub-Recipes with yield in eaches and a quantity different than 1 were

not calculating correctly.

UpMeals_1.0.20221114

Added

  • Include total weight for each meal in the Execution Worklist
  • Add amounts to "Used In" section in Daily, Pre-Ex, and Bulk worklists
  • Show invoices in Worklist screens

Changed

  • Make Kitchen Production data errors more visible, to prevent them from passing undetected
  • Layout updates to Kitchen Production screens

Fixed

  • When deleting a Meal's UPC, and saving the meal, a new UPC is generated. This was't working properly, fixed it.
  • Fix bugs with invoice date handling logic of QBO vs local cache
  • UI inconsistencies when deleting a UPC code in Meals
  • Ingredient Purchase Unit choices were being limited to standard usage units. Changed to include full list of units.

UpMeals_1.0.20221109

Added

  • Added no tags message for meals and sub-recipes
  • Added OneMeal column to the pdf which is generated when Print is clicked in execution worklist
  • Automatic reload of Worklist data when tab becomes active and production day is in the past
  • Added alert dialog when user changes verified data to unverified
  • Added vendor column to Ingredient List table
  • Added disclaimer in print view if worklist is printed before cutoff
  • Added JSON templates to labels
  • Added Label Design tab to products
  • Label template generator that parses label JSON templates and generates a basic HTML view of the label

Changed

  • Exclude inactive meals from kitchen worklists
  • Show "Inactive Meals in Invoices" in the Errors report
  • Show some more details for errors: Quantities and Customers
  • Changed unverified indicator color from gray to red
  • Scroll to top when clicking on sidebar links
  • Changes to packaging fields: added vendors, image upload, active status and more
  • Changes to label fields: label shape and dimensions that adjust accordingly
  • "Used In" section now shows ALL the meals/sub-recipes that the ingredient/recipe is being used in,

regardless of the nesting level

  • Removed auto-unverification on changes to non-label fields

Fixed

  • Reloading data when switching between Worklists was buggy and sometimes gave wrong data until a full refresh. Fixed.
  • Delete invoices from cache that no longer exist in QBO
  • Type check errors on ingredient bulk editor
  • An empty UPC code is allowed to be saved

UpMeals_1.0.2022110302

Added

  • Added auto calculate meal weight from ingredients
  • Configuration to run the project connecting to the Staging API
  • Added chevrons for Export to Excel and Print buttons when grouped by Components
  • Added validation for Total Weight in edit mode in Sub Recipe
  • Added sorting in query params in management screens
  • Added Qbo Prefix field in Customers page
  • Added SKUs in Edit Customer page(Product Lineup tab) also in the Product page(Customers tab)

Changed

  • Changed relation package to point to a Package instead of a Tag
  • Updated ingredient view UI and made tags and Nutrition Data display to be conditional

Fixed

  • Alignment of Kitchen Production dropdown filters was off, sometimes disappearing to the right of the viewport. Fixed.

UpMeals_1.0.20221029

Fixed

  • Wrong invoice dates when loading invoices from local cache
  • Worklist filters not being applied to new data from server

UpMeals_1.0.20221028

Fixed

  • A warning for Nutrition Data Missing was being shown whenever a new ingredient was added to a recipe. Fixed.
  • Fix problems with stale QBO Invoice cache.

UpMeals_1.0.20221024

Added

  • Add delivery dates to Packing worklist
  • Backend logic to return computed nutrition data for recipe components
  • Added upload manufacturer specs functionality and added brand field
  • Added endpoint for creating a product for each active meal that doesn't belong to any products

Changed

  • Layout and style adjustments to recipe view/edit

Fixed

  • Fixed some issues with customer filters in Packing worklist

UpMeals_1.0.20221018

Fixed

  • Fixed empty SVG barcode download bug

UpMeals_1.0.20221017

Added

  • Badge for meals with verified data for label print
  • Automatic system for flagging as "unverified" if there is a change in the meal or any of its components
  • Added filter icon in the FilterDropdownComponent

Changed

  • Barcode SVG gets scaled to a custom size before dowload

Fixed

  • Lots of useless audit log entries not being generated anymore
  • Allergen string is copied in the order that it is displayed
  • May contain copy to clipboard functionality fix

UpMeals_1.0.20221012

Added

  • Added "Organizing mode" to worklists (group by recipe components)
  • Every recipe component can be optionally excluded from organizing.
  • There's a new global setting for Ingredients and Sub-Recipes to be included or

excluded from organizing lists by default (can be overriden)

Fixed

  • Wrong weight was displayed for sub-recipes with yield in "eaches"
  • Kitchen Worklists Print preview performance improved significantly

UpMeals_1.0.20221011

Changed

  • Add total weight to Meal NFPs. Also some small style and copy fixes.

UpMeals_1.0.2022100602

Fixed

  • Fixed false positives while checking circular dependencies for nutrition data calculations

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.

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

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

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

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

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

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

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

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

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.

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

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)

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

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

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

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.

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

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

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

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

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

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

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

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

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

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

UpMeals_1.0.20220308

Changed

  • Enable Data Import tools in Production

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

Removed

  • Duplicate users controller
  • Customer location section

UpMeals_1.0.0

Added

  • Initial release
  • List of features included:
  • Add/Edit Ingredients
  • Add/Edit Ingredient Procurements
  • Add/Edit Vendors
  • Add/Edit Recipes
  • Add/Edit metadata: Recipe Categories, Ingredient Categories, etc
  • Add/Edit Tags and Tag Categories
  • Kitchen Production Lists: Daily Prep, Pre-Execution, Execution, Bulk
  • Export to Excel for any Kitchen Production List
  • Print view for any Kitchen Production List
  • User login
  • QBO OAuth screens and flow

[4.0.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv3.0.21&targetVersion=GTv4.0.0 [3.0.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.4.0&targetVersion=GTv3.0.0 [2.4.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.3.2&targetVersion=GTv2.4.0 [2.3.1]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.3.0&targetVersion=GTv2.3.1 [2.3.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.2.4&targetVersion=GTv2.3.0 [2.2.1]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.2.0&targetVersion=GTv2.2.1 [2.2.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.1.1&targetVersion=GTv2.2.0 [2.1.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv2.0.0&targetVersion=GTv2.1.0 [2.0.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals/branchCompare?baseVersion=GTv1.0.20221201&targetVersion=GTv2.0.0 [1.0.0]: https://dev.azure.com/upmeals/upmeals/_git/upmeals?version=GTv1.0.0_MVP