Skip to content

Protocol Raw Operations Portal Documentation

Version: 5.0.1
Last Updated: April 18, 2026
Status: Production Ready
Portal URL: https://ops.protocolraw.co.uk
Governing SOP: SOP-OPS-01 v1.2 Operations Self-Service Actions


What's New in v5.0

Sidebar navigation restructure: - Replaced the flat horizontal tab bar with a grouped left sidebar. Four semantic groups: Operations, Product, Customer, Intelligence. - Collapsible sidebar (240px expanded ↔ 56px collapsed) with state persisted to localStorage. - Collapsible sections within the sidebar, also persisted per-group. - Mobile: sidebar becomes an overlay triggered by a fixed hamburger button. Backdrop tap, Escape key, or tab selection all dismiss it. Body scroll locked while open; focus moves into the sidebar for keyboard users, and back to the hamburger on close. - Analytics is now an external link in the sidebar — opens Metabase in a new tab with an external-link icon, no content section required. - Testimonials moved under the Customer group (previously sat alongside unrelated items in the flat nav). - Keyboard shortcuts 19 jump directly to a sidebar item (previously no per-tab shortcuts existed below Ctrl/Cmd+N). Escape dismisses the mobile sidebar. - New files: css/sidebar.css, js/sidebar.js. Old tab-bar CSS removed from css/layout.css. - v5.0.1 polish: body scroll lock on mobile, Escape to close, focus management for a11y, orphaned old nav CSS cleanup (~260 lines removed).


What's New in v4.4

Dog Photos moderation system (SOP-PHOTO-01 v1.0): - Dog Photos tab — moderation queue for customer-submitted dog photos with channel-scoped consent (website, social, ads) - New raw_ops.dog_photos table with status workflow (submitted, approved, featured, rejected, revoked) - New public.v_dog_wall_display view powering the future public Dog Wall - Four RPC functions: submit_dog_photo, get_dog_photos_queue, moderate_dog_photo, revoke_dog_photo_consent - New dog-photo-upload Edge Function for multipart uploads to the private dog-photos storage bucket - Terminal revoke action with operator reason capture, logged to ops_events - Config version bumped to 4.4


What's New in v4.0

Operations Self-Service (SOP-OPS-01 v1.2): - ✅ System Health tab — 13-tile morning health dashboard with red/amber/green status, click-through to 7 exception queue panels - ✅ Exception queues — Lab Quarantine, Stuck Refunds, Failed Sync Events, Email Delivery, Stuck Proof Jobs, Payment Recovery, Referral Fraud Review - ✅ Worker Controls — 5 run-now buttons for stalled background jobs with 60s per-worker cooldown - ✅ Scheduled Tasks — live status of every critical cron job (healthy / stale / overdue / failed / missing) - ✅ Order Replay — pull a missing Shopify order when the webhook never arrived - ✅ Diagnostics — 10 canned read-only reports with column sort, filter search, and show-all expansion - ✅ Manual Lab Result Entry modal — full SOP Part 4 form with PDF upload and client-side PASS+detected-pathogen hard block - ✅ Compliance tab — 5 recording forms (temperature audit, proof spot check, HACCP evidence pack, lab sample submission, rejected batch disposition) - ✅ Agent identity resolved server-side from the Cloudflare Access JWT; no free-text actor fields anywhere in the mutation surface

Performance fixes: - ✅ Dashboard split into 4 parallel RPCs (ops_health_pipeline / product / customer + ops_get_cron_health); tiles render progressively as each group resolves - ✅ raw_ops.mv_cron_last_runs matview over cron.job_run_details, refreshed every 2 min — Scheduled Tasks tile returns in ~200ms vs. 9s seq scan - ✅ Dashboard functions converted to LANGUAGE sql STABLE for planner-friendly plan caching - ✅ raw_ops.v_email_outbox view over the canonical raw_ops.outbox table, exposing the SOP-expected column names for the email-delivery tile and retry path


What's New in v3.8

Terminology & Schema Corrections: - ✅ Batch status terminology alignedCLEAREDRELEASED, QA_FAILQUARANTINE (matches live database, SOP-LAB-01 v4.3, config.js, and Systems Overview v2.0) - ✅ Database schema updated — Removed stale CHECK constraint from documentation (live database has no status enum constraint); updated column names to match actual schema - ✅ Batch lifecycle diagram corrected — All status references now use canonical values - ✅ Version aligned to live portal — Documentation now matches portal v3.8 (Refund Actions)

From v3.7 (February 2026): - ✅ Refund action buttons in Support Workstation - ✅ Config version bumped to v3.8

From v3.6 (January 20, 2026): - ✅ Conversation Thread Panel - Full message history displayed in ticket detail view - ✅ Email Threading - In-Reply-To and References headers for proper inbox threading - ✅ [Support] Subject Prefix - All outbound emails prefixed to distinguish from marketing - ✅ Editable Subject Line - Agent can customize subject before sending - ✅ Message History Tracking - ticket_messages table records full back-and-forth - ✅ Unified Email Sending - send-support-email Edge Function handles all outbound

Database Updates: - ✅ email_message_id and email_references columns on support_tickets - ✅ ticket_messages table with direction, sender, delivery_id fields - ✅ Updated v_support_queue view with message count

From v3.5 (January 19, 2026): - ✅ Agent Review tab - Human-in-the-loop validation interface for AI agent decisions - ✅ Shadow Mode workflow - All AI decisions queued for human review before autonomous mode - ✅ Confidence badges - Color-coded display (green >85%, yellow 70-85%, red <70%) - ✅ cs_agent_decisions table - New database table for AI decision tracking

From v3.4 (January 13, 2026): - ✅ Documentation consolidation - merged v2.0 and v3.3 into single comprehensive document - ✅ Pack Day, Fulfillment, and Batch Creation workflows restored - ✅ Full troubleshooting guide restored (20+ scenarios)

From v3.3 (December 10, 2025): - ✅ Customer Search - Real-time search by name, email, or phone with debounced queries - ✅ Customer Profile Panel - Full customer view with subscription management, order history, address - ✅ Ticket Assignment - Assign tickets to agents with visible queue badges - ✅ Outbound Tickets - Proactively contact customers from Customer Profile - ✅ Awaiting Reply Filter - Dedicated queue filter for outbound tickets awaiting customer response


Table of Contents

  1. Overview
  2. Access & Authentication
  3. Portal Structure
  4. Architecture Overview
  5. System Health Module (SOP-OPS-01)
  6. Compliance Module (SOP-OPS-01)
  7. Pack Day (SOP PACK)
  8. Fulfillment (SOP-ORD-03)
  9. Batch Creation (SOP 01)
  10. Support Workstation
  11. Agent Review (Shadow Mode)
  12. Dog Photos
  13. Live Chat
  14. Analytics
  15. Database Views & Tables
  16. Edge Functions
  17. Make.com Webhooks
  18. System Architecture
  19. Security & Best Practices
  20. Troubleshooting
  21. Maintenance & Updates
  22. Keyboard Shortcuts
  23. Deployment
  24. Appendix
  25. For LLM Assistants

Overview

Purpose

The Protocol Raw Operations Portal is a unified interface for managing core business operations. Built as a tech-first, AI-native platform, it demonstrates operational sophistication and provides a single hub for packing, fulfillment, batch tracking, customer support, AI agent validation, and business intelligence.

Key Features

  • Pack Day Automation (SOP PACK) - Weather-based PCM calculation for Tuesday/Thursday packs
  • Fulfillment Management (SOP-ORD-03) - Batch dispatch recording with CSV upload
  • Batch Creation (SOP-LAB-01) - Production batch registration with QA_HOLD workflow
  • Support Workstation - Full customer operations with ticket management, search, conversation threading, and proactive outreach
  • Agent Review - Shadow mode validation for AI customer service decisions
  • Live Chat - Real-time customer communication
  • Analytics Integration - Metabase dashboard access
  • Protocol Raw Branding - Consistent visual identity throughout
  • Responsive Design - Works on desktop, tablet, and mobile
  • Real-time Updates - Direct Supabase database connectivity

Design Principles

  1. Scale First - Built for 100,000+ customers from day one
  2. Zero Dependencies - No npm packages, no build step, no dependency rot
  3. Modular by Default - Each feature is a self-contained ES6 module
  4. State-Driven UI - Centralized state with pub/sub for reactive updates
  5. Brand Consistent - CSS tokens derived from Visual Identity Guide v2.1
  6. Systematic, not emotional - Evidence-based operations
  7. Proof, not promises - Verifiable safety at every step

Access & Authentication

Portal URL

Primary: https://ops.protocolraw.co.uk
Backup: https://protocol-raw-ops.pages.dev

Authentication Method

The portal uses Cloudflare Access with Google OAuth for authentication. Only authorized email addresses can access the portal.

Login Process

  1. Navigate to https://ops.protocolraw.co.uk
  2. Click "Sign in with Google"
  3. Authenticate with your authorized Google account
  4. Session lasts 24 hours before re-authentication required

Managing Access

Add/Remove Users: 1. Cloudflare Dashboard → Zero Trust → Access → Applications 2. Find "Protocol Raw Operations Portal" application 3. Edit policy → Add or remove email addresses 4. Save changes

⚠ï¸Â Security Note

The portal contains your Supabase service role key with full database access. It is protected by Cloudflare Access authentication. Never share login credentials or disable authentication.


Portal Structure

The portal uses a grouped sidebar navigation (v5.0) with four semantic groups. System Health is the default landing tab for the morning check routine. Sidebar state (collapsed/expanded + which section groups are collapsed) persists across sessions via localStorage.

Operations 1. ❤️ System Health (SOP-OPS-01) — 13-tile health dashboard, exception queues, worker controls, diagnostics (default landing tab) 2. 📋 Compliance (SOP-OPS-01 Part 7) — daily compliance recording: temperature audit, spot checks, HACCP evidence, lab samples, batch disposition 3. 🎯 Pack Day (SOP PACK) — Automated packing workflow 4. 📦 Fulfillment (SOP-ORD-03) — Dispatch recording 5. 📊 Inventory — Stock levels and allocation

Product

  1. 🧪 Batches (SOP-LAB-01) — Production batch registration
  2. 💷 Cost Tracking (SOP-FIN-02) — Supplier invoices, cost allocation, unit economics

Customer

  1. 🎧 Support Workstation — Customer operations hub
  2. 🤖 Agent Review — AI decision validation (shadow mode)
  3. 💬 Live Chat — Real-time customer communication
  4. 🐕 Dog Photos (SOP-PHOTO-01) — Customer-submitted photo moderation queue
  5. ⭐ Testimonials — Customer testimonial review and approval

Intelligence

  1. 📈 Analytics — External link to Metabase dashboards (opens in a new tab; no in-portal content section)

Visual Design

Typography: - Headlines: Montserrat Bold 700 - Body text: Inter Regular 400 - UI elements: Inter Medium 500

Colors: - Background: Old Lace (#FEFDF7) - Surface: Spring Wood (#F2EEE3) - Text: Heavy Metal (#323531) - Muted text: Friar Gray (#7C7C78) - Primary CTA: Terracotta (#D48762) - Secondary: Nandor (#4B685B)

Layout: - Max width: 1200px (centered) - Generous padding: 40px desktop, 20px mobile - Border radius: 12px - Responsive breakpoint: 768px

Sidebar (v5.0): - Expanded: 240px wide. Collapsed: 56px icon-only rail. - Background: Spring Wood (#F2EEE3) — same tone as the page surface, so the sidebar reads as a structural frame rather than a chrome element. - Active item indicator: 3px left border in Terracotta (#D48762) plus a tinted background wash. - Section labels: uppercase 10px Inter, 8% letter-spacing, Friar Gray. - Badges: red (support urgent), amber (agent review pending), muted (informational counts). - Mobile (≤768px): sidebar becomes a slide-in overlay with a backdrop. Toggle button (desktop) is hidden; fixed hamburger button appears top-left.


Architecture Overview

Technology Stack

Layer Technology Purpose
Hosting Cloudflare Pages Static file serving, edge caching
Auth Cloudflare Access Zero-trust authentication
Database Supabase PostgreSQL Data storage, views, RPC functions
API Supabase REST + Edge Functions Data operations
Automation Make.com Webhook-triggered workflows
Subscriptions Seal Subscriptions Shopify subscription management
Email Customer.io Transactional email delivery
Frontend Vanilla JS (ES6 modules) No framework overhead
Fonts Google Fonts Montserrat + Inter

Request Flow

User Action
Module Function (e.g., support.subscriptionAction)
API Layer (api.js) → Supabase REST/RPC/Edge Function
    ↓                    or
                   Make.com Webhook → Seal API / Customer.io
State Update (state.js)
UI Re-render (subscribers notified)

File Structure

/
├── index.html
├── css/
│   ├── tokens.css              # Design tokens
│   ├── layout.css              # Layout + shared structural rules
│   ├── components.css          # UI components
│   ├── sidebar.css             # Sidebar navigation (v5.0)
│   ├── system-health.css       # System Health tab styles
│   ├── compliance.css          # Compliance tab styles
│   ├── dog-photos.css          # Dog Photos tab styles
│   └── agent-review.css        # Agent Review tab styles
└── js/
    ├── app.js                  # Entry point, module registry, tab callbacks
    ├── api.js                  # Supabase REST/RPC/Edge Function wrappers
    ├── config.js               # Centralised config (v5.0.1)
    ├── state.js                # Pub/sub state management
    ├── ui.js                   # UI utilities, custom confirm/prompt modals
    ├── sidebar.js              # Sidebar navigation controller (v5.0)
    └── modules/
        ├── pack-day.js         # Pack Day tab
        ├── fulfillment.js      # Fulfillment tab
        ├── batches.js          # Batches tab
        ├── inventory.js        # Inventory tab
        ├── support.js          # Support Workstation
        ├── agent-review.js     # Agent Review tab (v3.5)
        ├── live-chat.js        # Live Chat
        ├── testimonials.js     # Testimonials
        ├── dog-photos.js       # Dog Photos (v4.4)
        ├── cost-tracking.js    # Cost Tracking
        ├── system-health.js    # System Health (v4.0)
        └── compliance.js       # Compliance (v4.0)

System Health Module (SOP-OPS-01)

Purpose

Single-screen morning health check for the Ops Lead. Surfaces every automated system's current status at a glance, provides direct action queues when something needs attention, and exposes worker controls and diagnostics for deeper intervention. Governed by SOP-OPS-01 v1.2.

When to use: - Daily, first thing. Open the tab — if every tile is green, you're done in 30 seconds. - After an alert fires. Click the non-green tile that matches the alert to jump straight to the relevant exception queue. - When a background job appears stalled. Use Worker Controls to kick it manually. - When a customer reports "my order isn't in the system". Use Order Replay to pull the missing order from Shopify.

Health Tiles

Thirteen tiles sorted red → amber → green on each refresh. Tiles with matching exception panels become clickable when non-green and scroll to the panel on click. Inventory, Batch Testing, Support, and Deliveries tiles are informational — they surface problems that are handled in their existing dedicated tabs.

# Tile Monitors Green Amber Red
1 Order Pipeline Shopify webhook inbox 0 failed, on time 1–5 failed in 24h >5 failed OR SLO breach
2 Email Delivery v_email_outbox pending/failed <10 pending, 0 stuck 1–10 stuck OR 10–50 pending >10 stuck OR >50 pending
3 Inventory Released batch stock per SKU All products above reorder Any product in LOW band Any product CRITICAL
4 Batch Testing Batches in QA_HOLD 0 batches >3 days in QA_HOLD 1+ batch >3 days 1+ batch >5 days
5 Lab Processing lab_email_failures pending_review 0 quarantined 1–2 quarantined 3+ quarantined
6 Refunds Dead-lettered + failed refunds 0 failed or stuck 1–3 retrying Any stuck >1h or dead-lettered
7 Support Unassigned support tickets 0 unassigned >1h 1–3 waiting >3 waiting >1h
8 Subscriptions Seal webhook inbox 0 sync errors 1–5 sync errors >5 sync errors
9 Payments Subscriptions in dunning No stalls Day 7+ without email Day 10+ (imminent pause)
10 Deliveries Shipment exception rate (7d) <2% delivery problems 2–5% delivery problems >5% delivery problems
11 Referrals Fraud-flagged referrals 0 flagged 1–3 flagged >3 flagged
12 Proof Pages Last proof_portal_health monitor run Last check passed Last check >1h ago Last check failed
13 Background Jobs mv_cron_last_runs snapshot All critical jobs healthy 1+ stale (missed expected interval) 1+ overdue / failed / missing

Progressive loading: the dashboard fires four RPCs in parallel (ops_health_pipeline, ops_health_product, ops_health_customer, ops_get_cron_health). Tiles render as a pulsing loading skeleton until their group resolves, then flip to real state. A failed group shows "Unavailable" on just its own tiles; other groups keep working. Polling cadence: 60s, with a manual refresh button in the toolbar.

Exception Queue Panels

Seven panels live below the health grid. Clicking a non-green tile with a matching panel scrolls to and opens that panel. Panels are collapsed by default; click the header button to open or reload.

1. Lab Quarantine — pending review

Source: raw_ops.lab_email_failures WHERE status = 'pending_review'

When to use: Lab email parser couldn't process a certificate. The Lab Processing tile is amber or red.

Actions per row: - Retry — queue for re-parsing (only valid for batch_not_found, ai_parsing_failed, incomplete_parse). - Dismiss — mark as not a lab email; requires a reason. - Enter manually — opens the Manual Lab Result Entry modal with the quarantine row pre-linked (see below).

Infrastructure errors (storage_upload_failed, db_write_failed) are flagged escalate-to-founder — the retry button will refuse.

2. Stuck Refunds

Source: raw_ops.refunds WHERE dead_lettered_at IS NOT NULL

When to use: Refunds failed all automatic retries through Shopify. The Refunds tile is red.

Actions per row: - Send again — resets to pending; the on_refund_status_change trigger fires the processor within seconds. - Record manual refund — for refunds you processed directly in Shopify Admin. Takes the Shopify refund ID and a note; marks the record as processed.

3. Failed Sync Events (Shopify + Seal webhooks)

Source: webhook_inbox and seal_webhook_inbox WHERE processing_status = 'failed'

When to use: The Order Pipeline or Subscriptions tile is non-green.

Grouping: rows are grouped by the first 80 chars of their error message so you can see "8 are the same error: Connection timeout" at a glance. Each group has a select-all checkbox.

Actions: - Retry selected — up to 25 per click. Confirmation dialog warns that same-cause failures will likely fail again. - Retry next 10 (oldest) — takes the 10 oldest failed items from the currently loaded set.

4. Email Delivery

Source: raw_ops.v_email_outbox (over raw_ops.outbox WHERE kind = 'email') via ops_diagnostic_email_queue_detail

Two sections: - Failed emails — retryable. Retry failed emails button retries up to 25. - Stuck emails (pending, age ≥30 minutes) — cannot be retried (they're already pending). Indicates the email worker itself is stalled. Guidance points to Worker Controls → Run email sender now.

5. Stuck Proof Jobs

Source: raw_ops.proof_jobs WHERE state IN ('queued','failed') AND created_at < now() - 5 min

When to use: Batch created but no QR code or proof page generated. Proof Pages tile is red.

Action: Retry per row — resets state='queued', attempts=0, clears last_error. The process-proof-jobs cron picks it up within a minute.

6. Payment Recovery

Source: raw_ops.subscriptions WHERE payment_status IN ('failing','failed') AND status = 'ACTIVE' AND dunning_started_at IS NOT NULL via ops_diagnostic_dunning_pipeline

When to use: Payments tile is non-green.

Rows show days in dunning and which day-tier emails have already been sent. Day 10+ rows are highlighted red with "⚠ auto-pause tomorrow — consider calling the customer".

Actions: per row, Day 0 / Day 3 / Day 7 / Day 10 buttons mark the corresponding dunning email as sent (delegates to the existing fn_mark_dunning_action_sent_v1 with agent attribution).

7. Referral Fraud Review

Source: raw_ops.referrals WHERE status = 'fraud_flagged'

When to use: Referrals tile is amber or red.

Actions per row: - Approve — release the referral reward despite fraud flags (maps to status = 'confirmed'). - Reject — block the reward; requires a reason (maps to status = 'cancelled').

Both actions call public.manual_override_referral(p_referral_id, p_action, p_agent_email, p_reason) which delegates to the pre-existing raw_ops.manual_override_referral and writes an audit row to ops_events.

Worker Controls

Five one-click buttons to kick a background worker manually when it appears stalled. Each button has a 60-second cooldown after use (the button displays a live countdown) to prevent double-firing. Confirmation dialog before every invocation.

Button Edge Function When to use
Run email sender now process-outbox Emails stuck as pending; Email sender cron is stale or failed
Run proof generator now process-proof-jobs Proof jobs stuck queued for >5 min
Run webhook processor now shopify-webhook Webhooks stuck pending after retry
Run dunning processor now process-dunning Dunning emails not sending on schedule
Run lifecycle processor now send-lifecycle-events Lifecycle events not sending on schedule

All route through public.ops_trigger_worker_run(p_worker, p_agent_email)raw_ops.fn_invoke_edge_function.

Scheduled Tasks (Cron Status)

Live status of every critical cron job from the raw_ops.critical_cron_registry against raw_ops.mv_cron_last_runs (matview refreshed every 2 min). Summary counts (healthy / stale / critical / disabled) followed by a per-row table.

Health classification per job: - Healthy — last run within max_silence_minutes - Stale — didn't run when expected (between 1× and 2× threshold) - Overdue — significantly late (>2× threshold) - Failed — last run returned an error - Missing — registered in the registry but no matching cron.job - Never run — registered and scheduled but no runs in the last 24h - Disabled — cron job exists but active = false

Reload button re-fetches without affecting other panels.

Order Replay

When to use: Customer says their order went through but it's not in the system, AND webhook_inbox shows no failed webhooks for that order number (meaning the webhook never arrived).

How: Enter the Shopify order number as shown in Shopify Admin (e.g., #1042). The form calls public.ops_request_order_replay which invokes the replay-shopify-order Edge Function. The function fetches the order from Shopify Admin API, transforms to webhook payload format, and calls the standard ingestion pipeline. Expected to appear in the order list within 30 seconds.

The RPC refuses if the order already exists locally, to prevent accidental duplicate processing.

Diagnostics

Dropdown of 10 canned read-only reports. Results render as a sortable, filterable table. Defaults to first 50 rows with a Show all button that expands to the full result set (500-row cap). Column headers click to sort asc/desc; top-of-table search filters case-insensitively across every column.

Report Source Use case
Monitoring runs (24h) monitoring_runs Every monitor execution in the last 24h
Alert history (7d) monitoring_runs WHERE alert_triggered Which monitors have been firing
Failed webhooks (7d) webhook_inbox WHERE processing_status='failed' Detailed view for triage
Email queue detail v_email_outbox failed + stuck pending Deep-dive when email tile is non-green
Dunning pipeline subscriptions in dunning Full recovery state with day-tier flags
Recent refunds (30d) refunds Refund history for ad-hoc investigation
Batch status summary batches with SKU + age One row per batch, current state
Inventory by batch inventory joined to batches/products Per-batch stock with units + expiry
Recent ops events (24h) ops_events Unified audit stream for any entity type
Canary alerts (7d) monitoring_runs WHERE check_name ILIKE '%canary%' Infrastructure canary history

Manual Lab Result Entry Modal

Launched from the Lab Quarantine panel's Enter manually button. Governed by SOP-OPS-01 Part 4.

Design principle (SOP §4): the lab's official certificate verdict governs batch release, not a portal-side calculation. The operator enters what the certificate says; the system records it.

Form fields:

Field Type Required Notes
Batch Dropdown (QA_HOLD only) Yes Modal refuses to open if no batches are in QA_HOLD
Lab name Text (default: i2 Analytical) Yes
Certificate date Date (defaults to today) Yes
Certificate reference Text No Logged to ops_events meta
Lab verdict Dropdown: PASS / FAIL Yes Governing field
Salmonella (25g) Dropdown: Not Detected / Detected Yes Stored as salmonella_absent bool
Listeria (25g) Dropdown: Not Detected / Detected Yes Stored as listeria_absent bool
Enterobacteriaceae Number (cfu/g) Yes Stored as enterobacteriaceae_cfu_per_g
Lab certificate PDF File upload Yes Required for both PASS and FAIL. Uploaded to lab-reports/manual/<batch>/<timestamp>-<filename>.pdf

Safety guards (enforced client-side and server-side):

  1. PASS + Detected pathogen → hard block. If verdict=PASS and either Salmonella or Listeria is Detected, submit is refused with: "Cannot submit PASS with a detected pathogen. Check the certificate. If the lab certificate genuinely says PASS despite these values, escalate to the founder." The client-side check fires before any file upload happens.
  2. One result per batch. Unique index on lab_results(batch_id) prevents double-entry and prevents manual entry when an automated result already exists.
  3. Batch must be in QA_HOLD. Server rejects otherwise.
  4. PDF required for FAIL too. Failed-batch evidence matters as much as pass evidence for compliance.

Success path: INSERT INTO lab_results fires the existing on_lab_result_insert trigger, which handles batch status change (RELEASED or REJECTED), order allocation for PASS, proof portal update, and partner notification. The manual form is a different entry point into the same automated pipeline. Toast shows "Lab result recorded. Batch → RELEASED/REJECTED" and refreshes the quarantine panel + health tiles.


Compliance Module (SOP-OPS-01)

Purpose

Daily compliance recording for the team's accountable actions. Every submission is auditable via ops_events. Governed by SOP-OPS-01 v1.2 Part 7.

All five forms share the same patterns: - Agent email resolved from the Cloudflare Access JWT server-side — no free-text "Entered by" field. - File attachments upload to Supabase Storage (dispatch-files bucket) before the RPC fires; resulting paths are passed to the RPC as a TEXT[]. - Form clears on success; inline error box surfaces the server's rejection message if any.

Forms

1. Temperature Audit (SOP §7.1)

When: Weekly — after a shipment delivery, before the logger is wiped.

Fields: audit date, shipment dropdown (last 50 shipments by created date — UUIDs hidden, labelled courier · tracking · date · status), logger serial, max temp (°C), min temp (°C), time above −5°C (minutes), evidence file upload (photos + PDF), notes.

Live pass/fail preview computes on every input using the SOP formula: max ≤ −5 → PASS OR max ≤ 0 AND duration <30 min → PASS. The preview chip flips green/red as you type. The server runs the same check and the toast shows the authoritative verdict.

RPC: public.ops_record_temperature_audit

2. Proof Portal Spot Check (SOP §7.2)

When: Monthly spot check on a random recent batch.

Fields: check date, batch code, four checkboxes (QR code scans, page loads, results display correctly, PDF download works), notes.

Overall preview turns green only when all four checks are ticked; shows red "FAIL (not all checks passed)" for partial.

RPC: public.ops_record_proof_spot_check

3. HACCP Evidence Pack (SOP §7.3)

When: Every ship day (Tuesday and Thursday). Records that all evidence files for that day's dispatch are filed.

Fields: ship date, ship day type (Tuesday / Thursday / Special dispatch — values lowercase to match the evidence_packs.ship_day_type CHECK), six checkboxes (pick list filed, QC record filed, packout sheet filed, photo proofs filed, logger serials recorded, courier manifest filed), file uploads (any supporting docs), notes.

RPC: public.ops_record_evidence_pack

4. Lab Sample Submission (SOP §7.4)

When: Per batch — log samples sent to the lab immediately after the courier collects.

Fields: batch dropdown (filtered to QA_HOLD only; auto-disabled with helper text if none), sample sent date, lab name (default "i2 Analytical"), tracking reference, trays sampled (integer), notes.

RPC: public.ops_record_sample_submission — server re-checks QA_HOLD status and rejects otherwise.

5. Rejected Batch Disposition (SOP §7.5)

When: Per rejection — record what happened to the physical stock of a failed batch.

Fields: batch dropdown (filtered to REJECTED only), disposition action (Destroyed / Returned to supplier / Held for re-test), quantity (kg), disposition date, evidence files (photos/docs), notes.

Conditional fields — appear and become required only when "Destroyed" is selected: - Disposal method (e.g., "Commercial waste contractor") - Witnessed by (witness name)

Switching to Returned to supplier or Held for re-test hides these fields AND clears their values, so a stale entry can't survive an action switch. Both client-side and server-side enforce the destroyed → (method + witness) requirement as defence in depth.

RPC: public.ops_record_batch_disposition — also refuses if the batch isn't in REJECTED.

Unique Index Enforcement

batch_dispositions has UNIQUE (batch_id) — only one disposition record per batch. Attempting to resubmit surfaces the server's unique_violation error in the form's inline error box.


Pack Day (SOP PACK)

Purpose

Automate the intelligent packing workflow for Tuesday and Thursday dispatch days. Calculate PCM (Phase Change Material) requirements based on real weather forecasts to ensure frozen delivery.

Pack Schedule

Tuesday Dispatch: - Calculate PCM: 9:00 AM Tuesday - Pack orders: 10:00 AM - 4:00 PM - Courier collection: 4:00-6:00 PM - Customer delivery: Wednesday/Thursday (48h)

Thursday Dispatch: - Calculate PCM: 9:00 AM Thursday - Pack orders: 10:00 AM - 4:00 PM - Courier collection: 4:00-6:00 PM - Customer delivery: Friday/Saturday (48h)

Workflow

Step 1: Calculate PCM Requirements

When: Pack morning (9:00 AM on Tuesday or Thursday)

How: 1. Navigate to "Pack Day (SOP PACK)" tab 2. Click "🧊 Calculate PCM for Today's Packs" button 3. System executes: - Fetches all orders with export_state = 'sent' - Filters orders without existing packing instructions - Calls OpenWeatherMap API for each delivery postcode - Determines max temperature over next 48 hours - Calculates PCM requirements per packaging specification - Creates packing instructions in database - Returns results with risk levels

Expected Results:

✅ Success!
Processed 23 orders
23 successful, 0 failed

Results:
#1234 - 6 packs - STANDARD
#1235 - 9 packs - MEDIUM  
#1236 - 11 packs - HIGH

Risk Levels: - STANDARD (<20°C) - Base PCM requirement, no logger - MEDIUM (20-29°C) - Increased PCM, logger required - HIGH (≥30°C) - Maximum PCM, logger required

Troubleshooting:

Button doesn't respond: - Check browser console (F12) for errors - Verify Edge Function is deployed: calculate-pack-day-instructions - Check Supabase Edge Functions logs

Error: "No orders found": - Verify orders exist with export_state = 'sent' - Check SOP-ORD-02 (Export to Fulfillment) ran successfully - Query database: SELECT * FROM raw_ops.orders WHERE export_state = 'sent'

API error: - Check OpenWeatherMap API status - Verify API key in Edge Function is valid - Check Edge Function logs for detailed error

Step 2: View Pack Queue

When: After PCM calculation completes

How: 1. Click "📋 Open Pack Queue" button 2. System opens Metabase dashboard (when configured) 3. Dashboard shows: - Orders sorted by risk level (HIGH → MEDIUM → STANDARD) - PCM pack count per order - Delivery postcode - Customer name - Special notes

Alternative (Pre-Metabase):

Query the database directly:

SELECT 
  order_number_label,
  customer_name,
  delivery_postcode,
  box_size,
  pcm_packs_count,
  risk_level,
  ambient_forecast_max,
  logger_required,
  special_notes
FROM raw_ops.v_pack_queue_with_batch
ORDER BY 
  CASE risk_level 
    WHEN 'HIGH' THEN 1 
    WHEN 'MEDIUM' THEN 2 
    WHEN 'STANDARD' THEN 3 
  END,
  exported_at ASC;

Step 3: Pack & Dispatch

Process: 1. Follow pack queue instructions for each order 2. Record quality checks per SOP PACK-01 3. Use correct number of PCM packs 4. Insert temperature logger if required 5. Record dispatch via Fulfillment tab (SOP-ORD-03)

Quality Metrics: - Pack time: <10 mins (8kg/12kg), <12 mins (16kg) - Weight variance: ±5% tolerance - Temperature: Product at ≤-25°C at pack time

Technical Details

Edge Function: calculate-pack-day-instructions

Database Tables: - raw_ops.packing_instructions - Stores PCM requirements per order - raw_ops.orders - Source for orders ready to pack - raw_ops.v_pack_queue_with_batch - View combining instructions with batch data

API Integration: - OpenWeatherMap API (via Edge Function) - 1000 free calls/day - 48-hour forecast window - Postcode-based temperature lookup

Authentication: - Uses Supabase Anon Key (public, safe for browser) - Edge Function handles secure API calls


Fulfillment (SOP-ORD-03)

Purpose

Record dispatch confirmations for orders. Supports batch processing of dispatches with CSV upload to trigger automated courier integration and customer notifications.

Pack Day Export (SOP-ORD-02)

Added in Ops Portal v4.0 / SOP-ORD-02 v4.1

Export all eligible orders as a CSV pick list. Replaces the automatic 15-minute pg_cron export cycle during Phase A (founder-led fulfilment).

When: Start of pack day (Tuesday/Thursday), before packing begins

How:

  1. Navigate to "Fulfillment (SOP 0Y)" tab
  2. The "Pack Day Export" section at the top shows a live count of orders ready for export
  3. Click the 🔄 refresh icon to update the count
  4. Click "Export Pack Day Orders"
  5. Confirm in the dialog: "Export {count} orders?"
  6. System calls fn_trigger_3pl_export(500)export-3pl-dispatch Edge Function
  7. CSV email arrives at the founder email (TPL_EXPORT_EMAIL)
  8. Use CSV as the packing pick list

Eligibility criteria: Orders must be PAID, allocated, and matched to a RELEASED batch (see SOP-ORD-02 for full criteria).

Button states:

  • Enabled — count > 0 orders ready
  • Disabled (greyed out) — 0 orders ready for export

After export:

  • Count refreshes to show remaining orders (should be 0)
  • Orders transition from export_state = NULLqueuedsent
  • If email delivery fails, ord-02-retry-exports handles automatic retry with exponential backoff

Troubleshooting:

"No orders ready for export": - Verify orders exist with status = 'PAID' and export_state IS NULL - Check all order items are allocated to RELEASED batches - Query: SELECT COUNT(*) FROM raw_ops.v_orders_ready_for_export

Export button clicked but no email arrives: - Check exportMessage area for error details - Verify RESEND_API_KEY and TPL_EXPORT_EMAIL secrets in Supabase - ord-02-retry-exports will retry automatically (check outbox state)

Workflow

Step 1: Add Dispatches to Batch

When: After packing each order

How: 1. Navigate to "Fulfillment (SOP-ORD-03)" tab 2. Enter dispatch details: - Order ID: Shopify order ID (e.g., 12294292865399) - Tracking Number: Courier tracking number - Courier: Select from DPD, Royal Mail, Evri, UPS - SKU: Defaults to STARTER-8KG (or specify) - Qty: Number of units (usually 1) 3. Click "➕ Add to Batch" 4. Order appears in Current Batch table 5. Repeat for all orders packed today

Features: - LocalStorage Persistence: Batch survives page refresh - Duplicate Prevention: Can't add same Order ID twice - Remove Individual Orders: Remove button per row - Clear Batch: Clear all at once with confirmation

Example Batch:

Order ID Tracking Number Courier SKU Qty
12294292865399 DPD123456789 DPD STARTER-8KG 1
12294292865400 DPD123456790 DPD STARTER-12KG 1
12294292865401 DPD123456791 DPD STARTER-8KG 1

Step 2: Submit Batch

When: End of pack day (4:00-6:00 PM)

How: 1. Review Current Batch table (check for errors) 2. Click "🚀 Submit Batch" 3. Confirm submission in popup dialog 4. System executes: - Generates CSV file with all dispatches - Filename: dispatch_batch_YYYY-MM-DD_timestamp.csv - Uploads to Supabase Storage: dispatch-files/inbox/ - Clears batch from localStorage - Shows success message with filename

CSV Format:

external_order_id,dispatch_time_utc,courier,service,tracking_number,sku,qty,parcel_count,notes,status
12294292865399,2025-11-13T16:23:45.123Z,DPD,Standard,DPD123456789,STARTER-8KG,1,1,,DISPATCHED
12294292865400,2025-11-13T16:24:12.456Z,DPD,Standard,DPD123456790,STARTER-12KG,1,1,,DISPATCHED

Downstream Automation:

Once CSV is uploaded, poll-dispatch-files Edge Function (SOP-ORD-03) automatically: 1. Detects new file in dispatch-files/inbox/ 2. Parses CSV and validates data 3. Updates raw_ops.shipments table 4. Creates courier tracking records 5. Triggers customer notification emails 6. Moves file to dispatch-files/processed/

Step 3: Check Recent Activity

Purpose: Verify orders are ready for dispatch

How: 1. Click "Check Recent Orders" button 2. System queries last 20 orders with status queued or sent 3. Table displays: - Order ID - Export state (queued/sent) - Order date

Use Cases: - Verify SOP-ORD-02 exported orders successfully - Check order count matches expected pack day volume - Identify any orders stuck in queue

Technical Details

Storage Location: - Bucket: dispatch-files - Folder: inbox/ (for new files) - Folder: processed/ (after Make.com processes)

Database Tables: - raw_ops.orders - Source for recent activity - raw_ops.shipments - Updated by Make.com after CSV processing

Authentication: - Uses Supabase Service Role Key (full access, portal protected by Cloudflare Access)

File Naming Convention: - Format: dispatch_batch_YYYY-MM-DD_UnixTimestamp.csv - Example: dispatch_batch_2025-11-13_1731513825123.csv

Troubleshooting

"Order ID already in batch" error: - Order was already added to current batch - Check Current Batch table - Remove duplicate if needed

Submit fails with "Upload failed: 403": - Service Role Key may be invalid - Check Supabase Storage bucket permissions - Verify bucket dispatch-files exists with inbox/ folder

Submit succeeds but Make.com doesn't process: - Check Make.com scenario is active - Verify webhook URL is correct - Check scenario execution history for errors


Batch Creation (SOP 01)

Purpose

Register new production batches with automatic dual ID generation, QA_HOLD status, and proof portal integration.

Workflow

Create New Batch

When: Production batch arrives from co-packer

How: 1. Navigate to "Batches (SOP-LAB-01)" tab 2. Enter batch details: - Production Date: When batch was produced - Quantity (kg): Weight of batch 3. Click "Create Batch" 4. System executes: - Auto-generates internal batch code - Sets status: QA_HOLD - Sets partner: COPACKER-A - Sets COGS: £3.50/kg - Generates dual IDs (internal batch code + public batch ID) - Creates QR code for proof portal - Inserts record into raw_ops.batches - Shows success message

Example:

Input: - Batch Code: BATCH-20251113-001 - Production Date: 2025-11-13 - Quantity: 500 kg

Creates:

{
  "batch_code": "BATCH-20251113-001",
  "public_batch_id": "PR-A711BCF6",
  "production_date": "2025-11-13",
  "expiry_date": "2026-11-13",
  "kg_produced": 500.00,
  "status": "QA_HOLD",
  "partner": "COPACKER-A",
  "cogs_per_kg": 3.50
}

View Recent Batches

Purpose: Monitor batch status and inventory levels

How: 1. Click "View Recent Batches" button 2. System queries last 10 batches, sorted by production date 3. Table displays: - Batch Code - Public Batch ID - Production Date - Status (QA_HOLD, RELEASED, QUARANTINE, DEPLETED) - Quantity (kg)

Status Meanings: - QA_HOLD: Awaiting lab results (cannot be sold or allocated) - RELEASED: Lab tests passed, available for dispatch - QUARANTINE: Failed testing — do not use - DEPLETED: No stock remaining, archived

Batch Lifecycle

1. BATCH CREATED
   └─> Status: QA_HOLD

2. LAB SAMPLE SENT
   └─> Co-packer sends sample to UKAS lab

3. LAB RESULTS RECEIVED
   └─> Email with PDF arrives

4. SOP 01 AUTOMATION
   └─> Make.com parses PDF
   └─> Checks: Salmonella ABSENT, Listeria ABSENT
   └─> If PASS: Status → RELEASED
   └─> If FAIL: Status → QUARANTINE (batch quarantined)

5. BATCH AVAILABLE FOR SALE
   └─> Inventory system can allocate (SOP-INV-01)
   └─> Orders can be fulfilled

6. BATCH DEPLETED
   └─> All kg allocated to orders
   └─> Status → DEPLETED

Technical Details

Database Table: raw_ops.batches

Schema (simplified — see Supabase for full current schema):

CREATE TABLE raw_ops.batches (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  batch_code TEXT UNIQUE NOT NULL,
  public_batch_id TEXT UNIQUE NOT NULL,
  production_date DATE NOT NULL,
  expiry_date DATE,
  kg_produced NUMERIC NOT NULL CHECK (kg_produced > 0),
  qty_on_hand NUMERIC CHECK (qty_on_hand >= 0),
  qty_reserved NUMERIC CHECK (qty_reserved >= 0),
  status TEXT NOT NULL,  -- QA_HOLD, RELEASED, QUARANTINE, DEPLETED (no CHECK constraint)
  partner TEXT NOT NULL,
  cogs_per_kg_gbp NUMERIC CHECK (cogs_per_kg_gbp >= 0),
  total_cost_gbp NUMERIC CHECK (total_cost_gbp >= 0),
  formulation_id UUID REFERENCES raw_ops.formulations(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  released_at TIMESTAMPTZ,
  lab_cert_url TEXT
);

Note: The status column has no database-level CHECK constraint. Status transitions are enforced by application logic (SOP-LAB-01 triggers). The canonical status values are defined in config.js under BATCH_STATUSES: QA_HOLD, RELEASED, DEPLETED, QUARANTINE.

Authentication: - Uses Supabase Service Role Key (INSERT permission required)

Validation Rules: - Batch code must be unique - Production date cannot be in future - Quantity must be positive - COGS must be positive

Troubleshooting

"Batch code already exists" error: - Batch with this code already in database - Check Recent Batches to confirm - Use different batch code or verify co-packer confirmation

Create fails with "Permission denied": - Service Role Key may be invalid - Check Supabase console for key - Verify raw_ops.batches table exists

Expiry date incorrect: - System auto-calculates as production date + 12 months - Check production date was entered correctly - To manually adjust: Update in Supabase dashboard

Batch stuck in QA_HOLD: - Lab results not yet received - Check co-packer sent sample to lab - Check email for lab results PDF - Check lab_email_failures table for quarantined emails - Check monitoring_runs for lab_ingestion_health status


Support Workstation Module

File: js/modules/support.js
Purpose: Full-featured customer operations workstation with ticket management, customer search, conversation threading, and proactive outreach

Layout (v3.6)

┌─────────────────────────────────────────────────────────────────────────┐
│  Support Workstation                              [Refresh Queue]       │
├─────────────────────────────────────────────────────────────────────────┤
│  [ðŸâ€Â Search customers by name, email, or phone...]  [Clear]             │
├─────────────────────────────────────────────────────────────────────────┤
│  Customer Profile Panel (when customer selected from search)            │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ Customer Name        [📤 Contact Customer] [✕]                  │   │
│  │ email@example.com                                                │   │
│  │ Customer since: Jan 2025 · 5 orders · LTV: £450.00              │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ Subscription: Active                                            │   │
│  │ Next: Mon 16 Dec · Every 2 weeks · 8kg                          │   │
│  │ [Skip] [Pause] [Change Box ▾] [Change Frequency ▾]              │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ Address: 123 Example St, London, E1 1AA                         │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ Order History                                                    │   │
│  │ #1234 · 10 Dec · PAID · £89.00                                  │   │
│  │ #1198 · 26 Nov · PAID · £89.00                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────────┤
│  [Open: 5]  [High: 2]  [Resolved Today: 12]  [Avg: 15m]                │
├──────────────────┬──────────────────────────────────────────────────────┤
│  Queue List      │  Response Panel                                      │
│  ┌────────────┐  │  ┌─────────────────────────────────────────────────┐ │
│  │[All][📧][💬]│  │  │ Customer Context           Assigned To [▾]     │ │
│  │[📤 Awaiting]│  │  │ Name · Email · Dog · Sub   Waiting: 5m         │ │
│  ├────────────┤  │  ├─────────────────────────────────────────────────┤ │
│  │ Customer 1 │  │  │ CONVERSATION THREAD (v3.6)                      │ │
│  │ 5m · High  │  │  │ ┌───────────────────────────────────────────┐   │ │
│  │ 👤 Anton   │  │  │ │ â†Â john@example.com (10:15)                │   │ │
│  ├────────────┤  │  │ │ "Hi, I have a question about my order..." │   │ │
│  │ Customer 2 │  │  │ ├───────────────────────────────────────────┤   │ │
│  │ 15m · Med  │  │  │ │ → Sophie (10:45)                          │   │ │
│  │ 📤 outbound│  │  │ │ "Thanks for reaching out! I can help..."  │   │ │
│  │ Awaiting   │  │  │ ├───────────────────────────────────────────┤   │ │
│  ├────────────┤  │  │ │ â†Â john@example.com (11:02)                │   │ │
│  │ Customer 3 │  │  │ │ "Thanks, but I still have questions..."   │   │ │
│  │ 45m · Norm │  │  │ └───────────────────────────────────────────┘   │ │
│  └────────────┘  │  ├─────────────────────────────────────────────────┤ │
│                  │  │ AI Triage: Category · Confidence · Action       │ │
│                  │  ├─────────────────────────────────────────────────┤ │
│                  │  │ Subject: [Support] Re: My order question        │ │
│                  │  ├─────────────────────────────────────────────────┤ │
│                  │  │ Draft Response                                  │ │
│                  │  │ [textarea]                                      │ │
│                  │  ├─────────────────────────────────────────────────┤ │
│                  │  │ [Send Response] [Close Without Sending]         │ │
│                  │  └─────────────────────────────────────────────────┘ │
└──────────────────┴──────────────────────────────────────────────────────┘

Conversation Thread Panel (v3.6)

The Support Workstation now displays the full conversation history for each ticket:

Feature Status Notes
Inbound messages ✅ Built Customer emails with left alignment
Outbound messages ✅ Built Our replies with right alignment
Chronological order ✅ Built Oldest first
Sender display ✅ Built Customer email or persona name
Timestamp ✅ Built Relative time (e.g., "2h ago")
Message count badge ✅ Built Shows thread depth in queue

Data Source: raw_ops.ticket_messages

Visual Styling: - Inbound (customer): Left-aligned, gray background - Outbound (us): Right-aligned, green background

Email Response Features (v3.6)

Feature Status Notes
Editable subject line ✅ Built Pre-filled with [Support] prefix
[Support] prefix auto-added ✅ Built Distinguishes from marketing
Email threading ✅ Built In-Reply-To + References headers
Persona sign-off ✅ Built Sophie/Tom/Lucy auto-appended
Response logged to thread ✅ Built Stored in ticket_messages
Panel clears after send ✅ Built Ready for next ticket

Queue Filters

Filter Description
All All open tickets (inbound + outbound)
📧 Email Inbound email tickets only
💬 Chat Chat escalations only
📤 Awaiting Outbound tickets awaiting customer reply

Real-time search with 300ms debounce:

// Search triggers after 300ms of no typing
// Returns: customer_id, first_name, last_name, email, phone,
//          subscription_status, total_orders, lifetime_value

Search fields: first_name, last_name, email, phone (case-insensitive ILIKE)

Customer Profile Panel

Displays when customer selected from search:

  • Header: Name, email, customer since, order count, LTV
  • Subscription: Status, next delivery, box size, frequency, action buttons
  • Address: Shipping address
  • Order History: Last 10 orders with status and total
  • Contact Button: Opens outbound ticket modal

Ticket Assignment

  • Agents Table: raw_ops.agents with id, name, email, role, is_active
  • Dropdown: Shows active agents, updates ticket on selection
  • Queue Badge: Blue "👤 Anton" badge on assigned tickets
  • Persistence: Stored in support_tickets.assigned_to

Outbound Tickets

Proactive customer contact flow:

  1. Search for customer → Click "📤 Contact Customer"
  2. Modal opens with customer info pre-filled
  3. Select reason: Delivery Issue, Order Issue, Payment/Billing, Account Update, General Inquiry
  4. Enter subject and message
  5. Click "Send Email & Create Ticket"
  6. Ticket created with is_outbound=true, status='awaiting_reply'
  7. Email sent via Make.com → Customer.io
  8. Ticket appears in "Awaiting" filter with purple styling

Outbound Ticket Flow:

Customer Profile
[📤 Contact Customer]
Modal: Reason + Subject + Message
create_outbound_ticket() RPC
Make.com Webhook → Customer.io Email
Ticket in queue (status: awaiting_reply)
Customer replies → Inbound flow (status: open)

Email Send Flow (v3.9)

Operator clicks "Send Response"
Portal calls send-support-email Edge Function
Edge Function:
1. Fetches ticket (email_message_id, subject)
2. Normalises placeholder subjects ("(no subject)", "(pending)" → category fallback)
3. Applies [Support] prefix if missing
4. Adds Re: prefix only if prior outbound messages exist on ticket
5. Assigns persona (Sophie/Tom/Lucy) deterministically by customer email hash
6. Replaces [Name] placeholders with persona name
7. Appends persona sign-off ("{persona}\nProtocol Raw") to message body
8. Builds threading headers: In-Reply-To + References (plain object format)
9. Sends via Customer.io Transactional API (Support Layout)
10. Inserts message into ticket_messages (direction: outbound)
11. Updates ticket status to resolved
Customer receives threaded email with [Support] prefix and persona sign-off

Database Dependencies

Views: - public.v_support_queue - Open tickets with subscription context, assignment, outbound fields, message count

Tables: - raw_ops.support_tickets - Ticket records (includes is_outbound, outbound_reason, assigned_to, email_message_id) - raw_ops.ticket_messages - Conversation thread (v3.6) - raw_ops.agents - Agent roster - raw_ops.subscriptions - Subscription records - raw_ops.subscription_actions - Action log - raw_ops.orders - Order history - raw_ops.customers - Customer records

RPC Functions: - public.search_customers(search_term, result_limit) - Customer search - public.get_customer_profile(p_customer_id) - Full customer profile - public.get_customer_orders(p_customer_id, p_limit) - Order history - public.get_active_agents() - Active agents list - public.assign_ticket(p_ticket_id, p_assigned_to) - Assign ticket - public.create_outbound_ticket(...) - Create outbound ticket - public.log_subscription_action(...) - Log and sync subscription changes - public.get_ticket_messages(p_ticket_id) - Get conversation thread (v3.6)

Edge Functions: - send-support-email - Send response with threading (v3.6)


Agent Review Module

File: js/modules/agent-review.js
Styles: css/agent-review.css
Purpose: Human-in-the-loop validation interface for AI agent decisions during shadow mode

Purpose

The Agent Review tab provides a validation interface for AI customer service decisions before enabling autonomous responses. All AI decisions are queued for human review, allowing operators to approve correct decisions or reject incorrect ones with feedback for training.

Accessing

  1. Navigate to Ops Portal → 🤖 Agent Review tab
  2. Badge shows count of pending reviews
  3. Click Refresh to load latest queue

Review Workflow

1. AI processes inbound email → generates decision
2. Decision written to `cs_agent_decisions` with `review_status: 'pending'`
3. Decision appears in Agent Review queue
4. Human reviews:
   - Category assignment
   - Confidence score
   - AI reasoning
   - Draft response
   - Proposed actions
5. Human clicks **Approve** or **Reject**
6. Approved: Decision marked as validated
   Rejected: Feedback captured for training

UI Elements

Element Description
Review Card Shows AI decision details
Confidence Badge Color-coded (green >85%, yellow 70-85%, red <70%)
Queue Age Time since AI processed
Approve Button Mark decision as correct
Reject Button Mark incorrect with feedback prompt

Database Table

CREATE TABLE raw_ops.cs_agent_decisions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ticket_id UUID REFERENCES raw_ops.support_tickets(id),

  -- AI Processing
  category TEXT,
  confidence_score NUMERIC(3,2),
  reasoning TEXT,
  draft_response TEXT,
  proposed_actions JSONB DEFAULT '[]',
  escalation_reason TEXT,

  -- Processing timestamps
  processed_at TIMESTAMPTZ DEFAULT NOW(),

  -- Review status
  review_status TEXT DEFAULT 'pending' 
    CHECK (review_status IN ('pending', 'approved', 'rejected')),
  reviewed_at TIMESTAMPTZ,
  reviewed_by TEXT,
  rejection_reason TEXT,
  rejection_feedback TEXT,

  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_cs_agent_decisions_review_status 
  ON raw_ops.cs_agent_decisions(review_status) 
  WHERE review_status = 'pending';

CREATE INDEX idx_cs_agent_decisions_ticket 
  ON raw_ops.cs_agent_decisions(ticket_id);

JavaScript Module

File: js/modules/agent-review.js

Key Functions:

Function Description
loadQueue() Fetch pending decisions from database
renderQueue() Display decisions as review cards
approveDecision(id) Mark decision as approved
rejectDecision(id) Mark rejected with feedback
refresh() Reload queue and update badge
updateBadge() Update pending count in tab

App.js Integration

// Import
import agentReview from './modules/agent-review.js';

// Add to modules registry
const modules = {
  // ... other modules
  agentReview,
};

// Add to tab map
const tabModuleMap = {
  // ... other tabs
  agentReview: 'agentReview',
};

// Expose globally
window.AgentReview = agentReview;

Shadow Mode → Autonomous Mode Transition

Shadow Mode (Current): - All AI decisions queued for review - No automatic responses sent - Human approves/rejects each decision - Builds confidence in AI accuracy

Transition Criteria: - 95%+ approval rate over 100+ decisions - No critical errors in last 50 decisions - All edge cases documented

Autonomous Mode (Future): - High-confidence decisions auto-execute - Low-confidence still escalated - Human reviews exceptions only

Next Steps for Agent Review

  1. Wire up Make.com AI triage to insert into cs_agent_decisions
  2. Build confidence through shadow mode validation
  3. Define autonomous mode criteria
  4. Implement auto-execute for high-confidence decisions

Dog Photos Module

File: js/modules/dog-photos.js
Styles: css/dog-photos.css
Purpose: Moderation queue for customer-submitted dog photos with channel-scoped consent and terminal revocation
Governing SOP: SOP-PHOTO-01 v1.0

Purpose

The Dog Photos tab provides operators with a moderation interface for photos submitted by customers via the portal, founder email, support replies, health checkpoints, or manual uploads. Each submission captures separate consent flags for website, social, and advertising use. Moderators approve, feature, reject, or revoke photos. Only approved or featured photos with website consent render on the public Dog Wall.

Accessing

  1. Navigate to Ops Portal -> Dog Photos tab
  2. Badge shows count of photos in submitted status
  3. Click Refresh to reload the queue and signed thumbnail URLs

Moderation Workflow

1. Photo arrives via portal upload / founder email / support reply / checkpoint / manual
       |
2. Row inserted into raw_ops.dog_photos with status 'submitted'
       |
3. Appears in Dog Photos tab (submitted-first, LIMIT 100)
       |
4. Operator reviews thumbnail, caption, consent pills, flags
       |
5. Operator takes action:
   - Approve   -> status = 'approved'
   - Feature   -> status = 'featured' (pins to top of Dog Wall)
   - Reject    -> status = 'rejected' (rejection reason required)
   - Revoke    -> status = 'revoked' (terminal, reason captured)
       |
6. Approved/featured photos with consent_website = TRUE surface on Dog Wall
   once the minimum display threshold is reached

UI Elements

Element Description
Thumbnail 200x200 signed URL (1 hour TTL), object-fit cover, 8px border radius
Status Badge Colour-coded: submitted (yellow), approved (green), featured (blue), rejected (red), revoked (grey)
Consent Pills Three pills - Website, Social, Ads - showing on/off state captured at submission
Approve Button Sets status to approved, records moderated_by and moderated_at
Feature Button Sets status to featured, pins photo ahead of plain approved entries
Reject Button Requires rejection reason from dropdown; sets status to rejected
Revoke Button Prompts for reason, confirms terminal action; sets status to revoked, consent_revoked = TRUE
Refresh Button Reloads queue and recomputes signed URLs and badge count

Rejection Reasons

The rejection dropdown is bound to the database CHECK constraint:

Value Meaning
low_quality Blurry, dark, or otherwise unusable
not_a_dog Subject is not a dog
inappropriate Content unsuitable for brand surfaces
copyright_concern Likely not owned by submitter
human_consent Human in frame without captured consent
duplicate Already submitted
other Free-form

Database Dependencies

Table: raw_ops.dog_photos - customer-submitted photos, channel-scoped consent, moderation state. See SOP-PHOTO-01 for the full DDL.

View: public.v_dog_wall_display - anon-readable view joining approved/featured photos with customer tenure and order count, filtered on consent_website = TRUE and consent_revoked = FALSE.

Additional column: raw_ops.customer_health_checkpoints.photo_storage_path captures an optional photo attached to a health checkpoint submission (see SOP-HDI-01).

RPC Functions

Function Purpose
public.submit_dog_photo(...) Insert a new photo row, derive consent_method from source, log a DOG_PHOTO INFO event to ops_events. Returns the photo UUID.
public.get_dog_photos_queue() Return up to 100 photos where consent_revoked = FALSE and status in (submitted, approved, featured, rejected), submitted-first. Casts customers.email (USER-DEFINED domain) to text.
public.moderate_dog_photo(...) Update moderation fields with COALESCE so NULL arguments preserve existing values. Sets moderated_at and moderated_by only when p_status is non-null. Returns FALSE if the row is missing or already revoked.
public.revoke_dog_photo_consent(...) Terminal: sets status = 'revoked', consent_revoked = TRUE, consent_revoked_at = NOW(), stores the operator reason. Scoped by WHERE consent_revoked = FALSE.

Edge Function

dog-photo-upload (supabase/functions/dog-photo-upload/index.ts, deployed with --no-verify-jwt)

  • Endpoint: POST /functions/v1/dog-photo-upload
  • Content type: multipart/form-data
  • Required fields: photo (File), customer_id (UUID), dog_id (UUID), dog_name
  • Optional fields: consent_website (default true), consent_social (default false), consent_ads (default false), caption (max 100 chars)
  • Validation: UUID format, MIME in (image/jpeg, image/png, image/webp, image/heic), size at most 10 MB
  • Storage path: uploads/{customer_id}/{uuid}.{ext} in the dog-photos bucket, upsert = false
  • On RPC failure after upload, the orphaned storage object is removed
  • Returns { success: true, photo_id } on success, 400 for validation errors, 500 for storage or RPC failure

Storage Bucket

dog-photos (private, public = false, 10 MB file size limit, allowed MIME image/jpeg, image/png, image/webp, image/heic). The portal reads thumbnails via createSignedUrls(paths, 3600).

JavaScript Module

File: js/modules/dog-photos.js

Key Functions:

Function Description
loadQueue() Fetch rows via get_dog_photos_queue RPC
resolveSignedUrls(paths) Batch-resolve storage paths to 1-hour signed URLs
renderQueue() Render the queue as moderation cards
renderCard(row) Render a single card (thumbnail, consent pills, status badge, action row)
readCardFields(cardEl) Read operator-editable fields (caption, display order, flags) back from the DOM
moderatePhoto(id, patch) Call moderate_dog_photo RPC with the patch
approve(id) Shortcut for status approved
feature(id) Shortcut for status featured
reject(id) Prompt for rejection reason, then moderate with status rejected
revoke(id) Prompt for reason, confirm terminal action, call revoke_dog_photo_consent
refresh() Reload queue and update badge
updateBadge() Count rows with status = 'submitted' and set #dog-photos-badge

App.js Integration

// Import
import dogPhotos from './modules/dog-photos.js';

// Modules registry
const modules = {
  // ... other modules
  dogPhotos,
};

// Tab map
const tabModuleMap = {
  // ... other tabs
  dogPhotos: 'dogPhotos',
};

// Global namespace
window.opsPortal.dogPhotos = dogPhotos;

Display Threshold

The public Dog Wall surface on the website must not render until a minimum bank of approved photos exists. Current threshold: 20 approved photos. Collection and moderation continue prior to launch, but the Dog Wall is not published until the threshold is met. See SOP-PHOTO-01.


Live Chat Module

File: js/modules/chat.js
Purpose: Real-time customer communication with seamless escalation to ticketed support

Features

  • Real-time messaging via Supabase Realtime
  • Customer identification and context loading
  • Chat transcript history
  • Escalation to Support Workstation tickets
  • Typing indicators
  • Read receipts

Technical Details

See SOP CS-02: Live Chat System v1.4 for complete documentation.


Analytics

Purpose

Access Metabase business intelligence dashboards for operational insights and performance tracking.

Available Dashboards

Pack Queue with PCM Requirements - Orders ready to pack, sorted by risk level - PCM pack count per order - Delivery postcode and forecast temperature - Logger requirement indicator

Batch Status & Inventory - Current inventory levels (kg available) - Batches in QA_HOLD awaiting clearance - Expiry dates and batch rotation - FEFO (First Expired, First Out) compliance

Fulfillment Performance - Dispatch volume by day/week - Pack time metrics and quality checks - Courier performance and tracking - On-time delivery rates

CAC & LTV Metrics - Customer acquisition cost - Lifetime value projections - Box-2 retention rates - Cohort analysis

Access

How: 1. Navigate to "Analytics" tab 2. Click "Open Metabase Dashboard" 3. Opens in new browser tab 4. Authenticate with Metabase credentials (if required)

Metabase URL: (Configured in index.html)

Configuration

To add Metabase URL: 1. Open index.html in text editor 2. Find line: onclick="window.open('YOUR_METABASE_URL', '_blank')" 3. Replace YOUR_METABASE_URL with actual Metabase dashboard URL 4. Save and re-upload to Cloudflare Pages

Example:

onclick="window.open('https://metabase.protocolraw.co.uk/dashboard/1', '_blank')"


Database Views & Tables

Core Tables

Table Purpose
raw_ops.batches Production batch tracking
raw_ops.orders Customer order records
raw_ops.shipments Dispatch and tracking data
raw_ops.packing_instructions Pack Day PCM requirements
raw_ops.pack_quality_checks Quality control metrics
raw_ops.temperature_logger_data Post-delivery temperature audits
raw_ops.formulations Recipe management
raw_ops.customers Customer records
raw_ops.subscriptions Subscription records
raw_ops.support_tickets Support ticket records
raw_ops.ticket_messages Ticket conversation thread (v3.6)
raw_ops.ticket_notes Internal agent notes
raw_ops.agents Agent roster
raw_ops.cs_agent_decisions AI agent decisions (v3.5)
raw_ops.config System configuration
raw_ops.critical_cron_registry Registry of critical cron jobs with max_silence_minutes (v4.0, SOP-OPS-01)
raw_ops.temperature_audit_records Weekly temperature audit submissions (v4.0, SOP §7.1)
raw_ops.proof_portal_spot_checks Monthly proof portal spot checks (v4.0, SOP §7.2)
raw_ops.evidence_packs Per-ship-day HACCP evidence pack records (v4.0, SOP §7.3)
raw_ops.lab_sample_submissions Per-batch lab sample submission log (v4.0, SOP §7.4)
raw_ops.batch_dispositions Rejected batch disposition records, one per batch (v4.0, SOP §7.5)

Views (v4.0)

View Purpose
raw_ops.v_email_outbox SOP-OPS-01 compatibility projection over raw_ops.outbox WHERE kind='email', aliasing state → status, attempts → retry_count, error_message → last_error, target → to_email, event_type → template_key. Used by the email_delivery dashboard tile and the email retry path.

Materialized Views (v4.0)

Matview Purpose
raw_ops.mv_cron_last_runs DISTINCT ON (jobid) snapshot of cron.job_run_details with a unique index on jobid. Refreshed every 2 min by the refresh-cron-last-runs pg_cron job. Joined by fn_get_cron_health_summary_v1 so the Scheduled Tasks tile returns in ~200ms instead of timing out on a 645k-row seq scan.

Tables (v3.3+)

raw_ops.agents

CREATE TABLE raw_ops.agents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    email TEXT,
    role TEXT DEFAULT 'agent',  -- agent, admin, supervisor
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_agents_active ON raw_ops.agents(is_active) WHERE is_active = true;

raw_ops.support_tickets (columns added v3.3, v3.6)

-- v3.3 additions
ALTER TABLE raw_ops.support_tickets 
ADD COLUMN IF NOT EXISTS is_outbound BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS outbound_reason TEXT,
ADD COLUMN IF NOT EXISTS assigned_to TEXT;

-- v3.6 additions
ALTER TABLE raw_ops.support_tickets
ADD COLUMN IF NOT EXISTS email_message_id TEXT,
ADD COLUMN IF NOT EXISTS email_references TEXT;

CREATE INDEX idx_support_tickets_outbound 
ON raw_ops.support_tickets(is_outbound, status) 
WHERE is_outbound = true;

CREATE INDEX idx_support_tickets_assigned 
ON raw_ops.support_tickets(assigned_to) 
WHERE assigned_to IS NOT NULL;

Tables (v3.5)

raw_ops.cs_agent_decisions

CREATE TABLE raw_ops.cs_agent_decisions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ticket_id UUID REFERENCES raw_ops.support_tickets(id),

  -- AI Processing
  category TEXT,
  confidence_score NUMERIC(3,2),
  reasoning TEXT,
  draft_response TEXT,
  proposed_actions JSONB DEFAULT '[]',
  escalation_reason TEXT,

  -- Processing timestamps
  processed_at TIMESTAMPTZ DEFAULT NOW(),

  -- Review status
  review_status TEXT DEFAULT 'pending' 
    CHECK (review_status IN ('pending', 'approved', 'rejected')),
  reviewed_at TIMESTAMPTZ,
  reviewed_by TEXT,
  rejection_reason TEXT,
  rejection_feedback TEXT,

  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_cs_agent_decisions_review_status 
  ON raw_ops.cs_agent_decisions(review_status) 
  WHERE review_status = 'pending';

CREATE INDEX idx_cs_agent_decisions_ticket 
  ON raw_ops.cs_agent_decisions(ticket_id);

Tables (v3.6)

raw_ops.ticket_messages

CREATE TABLE raw_ops.ticket_messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ticket_id UUID NOT NULL REFERENCES raw_ops.support_tickets(id) ON DELETE CASCADE,
  direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
  sender TEXT NOT NULL,  -- customer email or agent persona
  subject TEXT,
  body TEXT NOT NULL,
  email_message_id TEXT,  -- For threading reference
  delivery_id TEXT,       -- Customer.io delivery ID for outbound
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_ticket_messages_ticket_id ON raw_ops.ticket_messages(ticket_id);

Ticket Statuses

Status Description
pending New ticket, not yet viewed
in_progress Ticket viewed, being worked on
awaiting_reply Outbound ticket waiting for customer response
resolved Issue resolved, response sent
closed Ticket closed without response

Key Views

  • v_pack_queue_with_batch - Pack Day queue with batch traceability
  • v_inventory_summary - Current inventory levels
  • v_order_fulfillment_status - Order status tracking
  • v_support_queue - Open tickets with customer context, message count (v3.6)

Tables (v4.0 — SOP-OPS-01)

The six new tables below back the System Health and Compliance tabs. All have RLS enabled (service role bypasses, anon blocked).

CREATE TABLE raw_ops.critical_cron_registry (
  job_name TEXT PRIMARY KEY,
  display_name TEXT NOT NULL,
  max_silence_minutes INTEGER NOT NULL,
  category TEXT NOT NULL DEFAULT 'operations',
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE raw_ops.temperature_audit_records (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  audit_date DATE NOT NULL,
  shipment_id UUID,
  logger_serial TEXT,
  max_temp_celsius NUMERIC NOT NULL,
  min_temp_celsius NUMERIC,
  duration_above_minus5_mins INTEGER,
  pass BOOLEAN NOT NULL,
  evidence_paths TEXT[],
  notes TEXT,
  recorded_by TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE raw_ops.proof_portal_spot_checks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  check_date DATE NOT NULL,
  batch_code TEXT NOT NULL,
  qr_scan_ok BOOLEAN NOT NULL,
  page_loads_ok BOOLEAN NOT NULL,
  results_display_ok BOOLEAN NOT NULL,
  pdf_download_ok BOOLEAN NOT NULL,
  notes TEXT,
  checked_by TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE raw_ops.evidence_packs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ship_date DATE NOT NULL,
  ship_day_type TEXT NOT NULL
    CHECK (ship_day_type = lower(ship_day_type)),
  pick_list_filed BOOLEAN NOT NULL,
  qc_record_filed BOOLEAN NOT NULL,
  packout_sheet_filed BOOLEAN NOT NULL,
  photo_proofs_filed BOOLEAN NOT NULL,
  logger_serials_recorded BOOLEAN NOT NULL,
  courier_manifest_filed BOOLEAN NOT NULL,
  file_paths TEXT[],
  notes TEXT,
  recorded_by TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE raw_ops.lab_sample_submissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  batch_id UUID NOT NULL REFERENCES raw_ops.batches(id),
  batch_code TEXT NOT NULL,
  sample_sent_date DATE NOT NULL,
  lab_name TEXT NOT NULL,
  tracking_reference TEXT,
  trays_sampled INTEGER,
  notes TEXT,
  recorded_by TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE raw_ops.batch_dispositions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  batch_id UUID NOT NULL REFERENCES raw_ops.batches(id),
  batch_code TEXT NOT NULL,
  disposition_action TEXT NOT NULL
    CHECK (disposition_action IN ('destroyed', 'returned_to_supplier', 'held_for_retest')),
  quantity_kg NUMERIC NOT NULL,
  disposal_method TEXT,
  witnessed_by TEXT,
  disposition_date DATE NOT NULL,
  evidence_paths TEXT[],
  notes TEXT,
  recorded_by UUID NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX idx_batch_disposition_one_per_batch
  ON raw_ops.batch_dispositions (batch_id);

Public RPC Surface (v4.0 — SOP-OPS-01)

Every mutation RPC follows the pattern public.ops_<action>(..., p_agent_email TEXT) → resolves agent via raw_ops.fn_resolve_ops_agent_v1 → delegates to raw_ops.fn_<action>_v1(..., p_agent_id UUID). Read RPCs have no agent resolution. All are GRANT EXECUTE TO service_role.

Read: ops_get_system_health, ops_get_cron_health, ops_health_pipeline, ops_health_product, ops_health_customer, ops_diagnostic_* (10 reports).

Exception actions: ops_retry_lab_quarantine, ops_dismiss_lab_quarantine, ops_manual_lab_result_entry, ops_retry_dead_lettered_refund, ops_resolve_refund_manually, ops_retry_selected_webhooks, ops_retry_failed_outbox, ops_mark_dunning_email_sent, ops_retry_stuck_proof_job, ops_trigger_worker_run, ops_request_order_replay, manual_override_referral.

Compliance recording: ops_record_temperature_audit, ops_record_proof_spot_check, ops_record_evidence_pack, ops_record_sample_submission, ops_record_batch_disposition.


Edge Functions

calculate-pack-day-instructions

Purpose: Calculate PCM requirements based on weather forecasts

Trigger: Portal Pack Day button

Process: 1. Fetch orders with export_state = 'sent' 2. Get 48-hour weather forecast per postcode 3. Calculate PCM pack count based on temperature 4. Create packing instruction records

send-support-email (v3.6)

Purpose: Send support responses via Customer.io with email threading

Trigger: Support Workstation "Send Response" button or CS-03 Agent

Process: 1. Receive ticket ID, customer email, subject, message 2. Fetch email_message_id from ticket for threading 3. Build headers: In-Reply-To + References if reply 4. Prepend [Support] to subject if not present 5. Select random persona (Sophie/Tom/Lucy) 6. Send via Customer.io Transactional API 7. Insert message into ticket_messages (direction: outbound) 8. Update ticket status to resolved 9. Return success with delivery_id and persona

Payload:

{
  "ticket_id": "uuid",
  "customer_id": "uuid",
  "customer_email": "customer@example.com",
  "subject": "Re: Original subject",
  "message": "Response body text",
  "source": "ops_portal | cs_agent",
  "delay_send": false
}

Response:

{
  "success": true,
  "delivery_id": "abc123",
  "persona": "Sophie",
  "threading_enabled": true,
  "sent_immediately": true
}

support-send-response (deprecated)

Purpose: Legacy email send function - replaced by send-support-email in v3.6

replay-shopify-order (v4.0)

Purpose: Pull a missing Shopify order through the standard ingestion pipeline when the original webhook never arrived.

Trigger: public.ops_request_order_replay via the System Health → Order Replay form.

Process: 1. Receive { "order_number": "#1042" }. 2. GET /admin/api/2024-01/orders.json?name=<order_number> against Shopify Admin API. 3. Transform the order payload into the canonical webhook shape. 4. Call fn_process_shopify_order_v3() — same function used by live orders/create webhooks. 5. Return success; the order appears in the orders list within ~30s.

Worker Controls target functions (v4.0)

The five Run * now buttons in System Health → Worker Controls each invoke an existing Edge Function via raw_ops.fn_invoke_edge_function. No new Edge Functions were added for Worker Controls — only the orchestration RPC public.ops_trigger_worker_run:

Button Edge Function
Run email sender now process-outbox
Run proof generator now process-proof-jobs
Run webhook processor now shopify-webhook
Run dunning processor now process-dunning
Run lifecycle processor now send-lifecycle-events

Make.com Webhooks

Portal Subscription Actions

Webhook URL: https://hook.eu2.make.com/25vurnbyc8bhvqi5zqvgf9mjf809z0m8

Scenario: "Portal Subscription Actions"

Routes:

Route Filter Action
Outbound Email action=outbound_email Customer.io transactional email
Ops Portal Frequency source=ops_portal, action=frequency_change Seal API edit intervalCount
Ops Portal Box Change source=ops_portal, action=box_change Seal API edit variantId
Ops Portal Skip source=ops_portal, action=skip Seal API skip billing attempt
Ops Portal Bypass source=ops_portal, action=pause/resume Seal API pause/resume

Outbound Email Route

Filter: {{1.action}} equals outbound_email

Payload:

{
  "action": "outbound_email",
  "ticket_id": "uuid",
  "customer_email": "customer@example.com",
  "customer_name": "John Smith",
  "subject": "Quick update about your delivery",
  "message": "Hi John, just wanted to let you know...",
  "reason": "Delivery Issue"
}

Customer.io API Call:

{
  "transactional_message_id": "support_response",
  "to": "{{1.customer_email}}",
  "identifiers": { "email": "{{1.customer_email}}" },
  "message_data": {
    "customer_name": "{{1.customer_name}}",
    "response_body": "{{1.message}}",
    "subject": "{{1.subject}}"
  },
  "from": "support@protocolraw.co.uk",
  "reply_to": "support@protocolraw.co.uk",
  "subject": "{{1.subject}}"
}


System Architecture

Database Architecture

Schema: raw_ops

Key Tables: - batches - Production batch tracking - orders - Customer order records - shipments - Dispatch and tracking data - packing_instructions - Pack Day PCM requirements - pack_quality_checks - Quality control metrics - temperature_logger_data - Post-delivery temperature audits - formulations - Recipe management - config - System configuration - cs_agent_decisions - AI agent decisions (v3.5) - ticket_messages - Conversation threads (v3.6)

Key Views: - v_pack_queue_with_batch - Pack Day queue with batch traceability - v_inventory_summary - Current inventory levels - v_order_fulfillment_status - Order status tracking - v_support_queue - Open tickets with message count (v3.6)

Access Levels: - Anon Key: Public read access + specific Edge Functions - Service Role Key: Full CRUD access (portal only, protected by Cloudflare Access)

API Integrations

Supabase REST API: - Base URL: https://znfjpllsiuyezqlneqzr.supabase.co/rest/v1/ - Authentication: Service Role Key in Authorization header - Schema: Accept-Profile: raw_ops

Supabase Edge Functions: - Base URL: https://znfjpllsiuyezqlneqzr.supabase.co/functions/v1/ - Authentication: Anon Key in Authorization header - Functions: calculate-pack-day-instructions, send-support-email

Supabase Storage: - Base URL: https://znfjpllsiuyezqlneqzr.supabase.co/storage/v1/ - Authentication: Service Role Key in Authorization header - Bucket: dispatch-files

External Dependencies

Google Fonts API: - Montserrat: fonts.googleapis.com/css2?family=Montserrat:wght@700 - Inter: fonts.googleapis.com/css2?family=Inter:wght@400;500;600

OpenWeatherMap API (via Edge Function): - Used for Pack Day weather forecasts - 1000 free calls/day - API key stored in Edge Function (not in portal)

Performance

Load Times: - Initial page load: <1 second - Tab switching: <50ms (instant) - Database queries: <100ms (indexed) - File uploads: ~1-2 seconds (depends on batch size) - Conversation thread load: <100ms (v3.6)

Scalability: - Built for 100,000+ customers - No hardcoded limits - Database indexed for performance - CDN-cached static assets


Security & Best Practices

Authentication & Authorization

Cloudflare Access Protection: - Portal protected by Google OAuth - Only authorized emails can access - 24-hour session expiry - Force re-authentication on sensitive operations

API Keys in Portal:

⚠ï¸Â Service Role Key is embedded in the HTML file

This key has full database access. Security measures:

  1. Protected by Cloudflare Access - Key only accessible to authenticated users
  2. HTTPS Only - All traffic encrypted in transit
  3. No public exposure - Portal URL not publicly listed
  4. Audit trail - All database operations logged
  5. Key rotation - Rotate key quarterly

Never: - Disable Cloudflare Access - Share portal URL publicly - Share login credentials - Access portal from untrusted devices - Take screenshots with sensitive data

Data Security

In Transit: - All connections use HTTPS/TLS 1.3 - API calls encrypted end-to-end - No unencrypted data transmission

At Rest: - Supabase database encrypted at rest - Storage files encrypted at rest - Automatic backups encrypted

Access Control: - Row-level security on sensitive tables - Service role key isolated to portal only - Edge Functions use limited anon key

Best Practices

Daily Operations: 1. Always log out after pack day (or let session expire) 2. Clear browser cache on shared devices 3. Never save passwords in browser on shared devices 4. Use private/incognito mode on shared devices 5. Report suspicious activity immediately

Batch Processing: 1. Double-check Order IDs before adding to batch 2. Review complete batch before submitting 3. Verify success message after submission 4. Keep paper backup of tracking numbers (Phase A) 5. Check Make.com processed successfully

Quality Control: 1. Verify PCM calculations match expected values 2. Cross-check pack queue against orders received 3. Monitor batch status transitions (QA_HOLD → RELEASED) 4. Review Recent Activity regularly for anomalies 5. Investigate any failed operations immediately

Incident Response: 1. Note exact error message and time 2. Check browser console for detailed errors (F12) 3. Check Supabase logs (Edge Functions, Storage, Database) 4. Document steps to reproduce 5. Check Make.com scenario execution history 6. Contact technical lead with details

Compliance

Data Protection: - Customer data (names, addresses) visible in portal - Covered by Protocol Raw privacy policy - GDPR compliance maintained - Data retention per policy

Food Safety: - Batch traceability maintained - Lab certificate links stored - Recall capability via batch_code - Quality check records preserved

Audit Trail: - All database operations timestamped - Created_at, updated_at on all records - ops_events log for critical operations - CSV files archived in Supabase Storage - Email conversation threads preserved (v3.6)


Troubleshooting

Common Issues

Portal Shows 404

Symptoms: Page not found error at ops.protocolraw.co.uk

Causes: 1. File not named index.html 2. File in wrong directory/folder 3. Deployment failed 4. DNS not propagated

Resolution: 1. Check Cloudflare Pages deployment status 2. Verify file is named exactly index.html 3. Check file is at root level (not in subfolder) 4. Try backup URL: protocol-raw-ops.pages.dev 5. Clear browser cache and retry

Can't Log In

Symptoms: Cloudflare Access blocks login

Causes: 1. Email not in authorized list 2. Session expired 3. Cookies blocked 4. Access policy changed

Resolution: 1. Verify email is in Cloudflare Access policy 2. Clear browser cookies for ops.protocolraw.co.uk 3. Try different browser 4. Check Cloudflare Access application is active 5. Contact admin to verify email whitelist

Pack Day Button Doesn't Work

Symptoms: Calculate PCM button shows error or nothing happens

Causes: 1. Edge Function not deployed 2. Edge Function error 3. No orders in sent state 4. Network connectivity issue 5. API quota exceeded

Resolution: 1. Open browser console (F12) and check for errors 2. Verify Edge Function exists: Supabase Dashboard → Edge Functions 3. Check Edge Function logs for errors 4. Query database: SELECT * FROM raw_ops.orders WHERE export_state = 'sent' 5. Check OpenWeatherMap API quota 6. Test Edge Function directly via Supabase dashboard

Fulfillment Upload Fails

Symptoms: Submit Batch shows error, CSV not uploaded

Causes: 1. Storage bucket doesn't exist 2. Service Role Key invalid 3. Network error 4. Bucket permissions incorrect

Resolution: 1. Check browser console for detailed error 2. Verify bucket exists: Supabase Dashboard → Storage → dispatch-files 3. Verify inbox/ folder exists in bucket 4. Check Service Role Key is correct 5. Test upload manually via Supabase Storage UI 6. Check bucket policies allow service role write access

Batch Creation Fails

Symptoms: Create Batch shows error, batch not in database

Causes: 1. Duplicate batch code 2. Service Role Key invalid 3. Table doesn't exist 4. Validation error 5. Database permissions

Resolution: 1. Check browser console for detailed error 2. Query database: SELECT * FROM raw_ops.batches WHERE batch_code = 'YOUR-CODE' 3. Verify table exists: Supabase Dashboard → Table Editor 4. Check all required fields are filled 5. Verify Service Role Key has INSERT permission 6. Try different batch code

LocalStorage Batch Lost

Symptoms: Current Batch empty after page refresh

Causes: 1. Browser cache cleared 2. Incognito/private browsing mode 3. Different browser/device 4. LocalStorage quota exceeded

Resolution: 1. Check you're using same browser as when batch was created 2. Don't use incognito/private mode during pack day 3. Re-add orders to batch (check Recent Activity for order IDs) 4. Submit batches more frequently to avoid loss 5. Keep paper backup of Order IDs during pack day (Phase A)

Customer Search Not Working

Symptoms: Search returns no results or errors

Causes: 1. RPC function missing 2. Database permissions 3. Network error

Resolution: 1. Check public.search_customers function exists 2. Verify function has EXECUTE granted to anon and service_role 3. Check browser console for RPC errors

Ticket Assignment Not Showing

Symptoms: Assignment dropdown empty or not visible

Causes: 1. No active agents in database 2. RPC function missing 3. Column missing on tickets table

Resolution: 1. Verify raw_ops.agents table has active agents 2. Check public.get_active_agents function exists 3. Verify assigned_to column exists on support_tickets

Outbound Tickets Not Sending Email

Symptoms: Ticket created but no email sent

Causes: 1. Make.com route missing 2. Customer.io template issue 3. Webhook error

Resolution: 1. Check Make.com scenario has outbound_email route 2. Verify Customer.io support_response template has {{subject}} in subject line 3. Check Make.com execution history for errors 4. Verify Customer.io API key is correct

Outbound Tickets Not Appearing in Queue

Symptoms: Created outbound ticket not visible

Causes: 1. Missing columns on tickets table 2. View not updated 3. Wrong status

Resolution: 1. Verify is_outbound and outbound_reason columns exist on support_tickets 2. Check public.v_support_queue view includes outbound fields 3. Verify ticket was created with status='awaiting_reply'

Agent Review Queue Not Loading

Symptoms: Agent Review tab shows no decisions or errors

Causes: 1. Table cs_agent_decisions doesn't exist 2. No pending decisions 3. RPC function missing

Resolution: 1. Verify raw_ops.cs_agent_decisions table exists 2. Check for pending decisions: SELECT * FROM raw_ops.cs_agent_decisions WHERE review_status = 'pending' 3. Verify module is correctly imported in app.js

Conversation Thread Not Loading (v3.6)

Symptoms: Thread panel shows "No messages" or errors

Causes: 1. Table ticket_messages doesn't exist 2. No messages for ticket 3. RPC function missing

Resolution: 1. Verify raw_ops.ticket_messages table exists 2. Check for messages: SELECT * FROM raw_ops.ticket_messages WHERE ticket_id = 'uuid' 3. Verify public.get_ticket_messages function exists 4. Check browser console for RPC errors

Email Not Threading (v3.6)

Symptoms: Customer sees separate emails instead of thread

Causes: 1. email_message_id not captured on ticket 2. Headers not sent 3. Customer.io template issue

Resolution: 1. Check ticket has email_message_id populated 2. Verify send-support-email Edge Function is deployed 3. Check Edge Function logs for header construction 4. Verify Customer.io template accepts headers parameter

Error Messages

"Failed to fetch" - Network connectivity issue - API endpoint unreachable - CORS error - Check internet connection - Try different network - Check Supabase status: status.supabase.com

"Authorization header required" - API key missing or invalid - Check configuration section of HTML - Verify Anon Key or Service Role Key is correct - Check key hasn't been rotated

"Permission denied" - Database RLS (Row Level Security) blocking operation - Service Role Key should bypass RLS - Check key is correct service role key (not anon key) - Verify key has correct permissions in Supabase

"Unique constraint violation" - Trying to create duplicate record - Common with batch codes - Check existing records - Use different identifier

"Network request failed" - DNS resolution issue - SSL certificate issue - Cloudflare error - Check Cloudflare status: cloudflarestatus.com - Try backup URL: protocol-raw-ops.pages.dev

Getting Help

Self-Service: 1. Check this troubleshooting section 2. Open browser console (F12) for detailed errors 3. Check Supabase logs (Edge Functions, Database, Storage) 4. Check Make.com scenario execution history 5. Query database directly via Supabase SQL Editor

Escalation: 1. Document exact error message 2. Note time of occurrence 3. Describe steps to reproduce 4. Check if issue is persistent or intermittent 5. Gather relevant screenshots (redact sensitive data) 6. Contact technical lead with details


Maintenance & Updates

Regular Maintenance

Daily (Pack Days): - [ ] Verify portal loads correctly - [ ] Test Pack Day calculation - [ ] Monitor fulfillment batch submissions - [ ] Check Make.com processed dispatches

Weekly: - [ ] Review Recent Batches for status changes - [ ] Check Recent Orders matches expected volume - [ ] Monitor browser console for JavaScript errors - [ ] Verify Cloudflare Access policy is current - [ ] Review Agent Review queue (clear any backlog)

Monthly: - [ ] Review Supabase API usage and quotas - [ ] Check OpenWeatherMap API call count - [ ] Audit user access list (add/remove as needed) - [ ] Review error logs for patterns - [ ] Test disaster recovery procedures - [ ] Review AI agent approval rates

Quarterly: - [ ] Rotate Supabase Service Role Key - [ ] Update portal with new key - [ ] Review and update user access policies - [ ] Performance audit and optimization - [ ] Documentation review and updates

Updating the Portal

To update portal files: 1. Make changes to local files 2. Push to GitHub repository 3. Cloudflare Pages auto-deploys from main branch 4. Verify changes at ops.protocolraw.co.uk 5. Test all functionality 6. Document changes in version history

Cache Busting

When updating JavaScript modules, bump the version parameter:

  1. In app.js: import support from './modules/support.js?v=N';
  2. In index.html: <script type="module" src="js/app.js?v=N"></script>

Current version: v18 (bumped for v3.8)

Deployment Checklist

  1. Update module files
  2. Bump version in app.js imports
  3. Bump version in index.html script tag
  4. Push to GitHub
  5. Cloudflare Pages auto-deploys
  6. Hard refresh browser (Ctrl+Shift+R)

Keyboard Shortcuts

Global

Single-key 19 shortcuts jump directly to the sidebar item. Shortcuts are suppressed while an <input>, <textarea>, or contenteditable element is focused, so they don't interfere with typing.

Shortcut Action
Tab Navigate between form fields
Enter Submit focused form
Escape Close modals / dismiss mobile sidebar
Ctrl/Cmd + F Find on page
1 System Health
2 Compliance
3 Pack Day
4 Fulfillment
5 Inventory
6 Batches
7 Cost Tracking
8 Support
9 Agent Review

Support Workstation

Shortcut Action
Ctrl/Cmd + Enter Send response
Escape Deselect ticket / Close modal

Agent Review

Shortcut Action
A Approve selected decision
R Reject selected decision (opens feedback)
N Next decision in queue

Deployment

Cloudflare Pages

Project: protocol-raw-ops
Domain: ops.protocolraw.co.uk
Backup: protocol-raw-ops.pages.dev

Auto-Deploy: - Connects to GitHub repository - Deploys on push to main branch - Build command: (none - static files) - Output directory: /

Manual Deployment (if needed)

  1. Download current files from repository
  2. Make necessary changes
  3. Test locally (open in browser)
  4. Cloudflare Pages → Create deployment → Upload assets
  5. Verify at ops.protocolraw.co.uk

Appendix

Browser Compatibility

Fully Supported: - Chrome 90+ - Firefox 88+ - Safari 14+ - Edge 90+

Mobile: - iOS Safari 14+ - Chrome Android 90+

Not Supported: - Internet Explorer (any version) - Opera Mini

File Size & Performance

  • HTML file size: ~35 KB
  • JavaScript modules: ~65 KB total (including agent-review.js, thread updates)
  • CSS files: ~15 KB total (including agent-review.css)
  • No external JavaScript dependencies
  • Google Fonts: ~50 KB (cached)
  • Total page weight: <165 KB
  • Load time: <1 second on 3G

Data Retention

LocalStorage: - Fulfillment batch: Until submitted or manually cleared - Browser-specific: Not synced across devices

Database: - Orders: Indefinite (business records) - Batches: Indefinite (traceability requirement) - Shipments: Indefinite (delivery records) - Packing instructions: 24 months (archived after) - Quality checks: 24 months (regulatory requirement) - Support tickets: Indefinite (customer service records) - Ticket messages: Indefinite (conversation history, v3.6) - AI agent decisions: 12 months (training data, then anonymized)

Backup & Recovery

Cloudflare Pages: - All deployments preserved - Can rollback to previous version instantly - Deployment history maintained indefinitely

Supabase Database: - Automatic daily backups - Point-in-time recovery (7 days) - Manual backup via Supabase dashboard

Critical Data: - Export batches table monthly (CSV) - Export orders table monthly (CSV) - Store batch lab certificates in cloud storage - Maintain paper backup of critical batch codes

Support Contacts

Portal Issues: - Cloudflare Support: https://support.cloudflare.com - Supabase Support: https://supabase.com/support

System Status: - Cloudflare: https://cloudflarestatus.com - Supabase: https://status.supabase.com - OpenWeatherMap: https://status.openweathermap.org

Internal: - Operations Lead: [contact info] - Technical Lead: [contact info] - QA Lead: [contact info]


For LLM Assistants

Adding New Features to Support Workstation

  1. Add RPC function in Supabase (public schema)
  2. Grant EXECUTE to service_role and anon
  3. Add JavaScript function in support.js
  4. Export function in default export
  5. Expose in app.js under window.opsPortal.support
  6. Add UI elements in index.html with onclick handlers
  7. Bump version numbers and deploy

Key File Locations

  • HTML: index.html (all tab sections)
  • JavaScript: js/modules/*.js (tab-specific logic)
  • CSS: css/*.css (component styles)
  • Global API: js/app.js (window.opsPortal.*)
  • Database: Supabase raw_ops schema + public RPC functions

Common Patterns

Adding a new tab module (v5.0 sidebar): 1. Create js/modules/newfeature.js 2. Create css/newfeature.css (if needed) 3. Import in app.js, add to the modules registry and tabModuleMap 4. Add <button class="sidebar-nav-item" data-tab="newfeature"> inside the appropriate .sidebar-section-items block in index.html (Operations / Product / Customer / Intelligence) 5. Add the corresponding <div id="newfeatureContent" class="tab-content"> content section inside <main class="ops-main"> 6. Add to window.opsPortal namespace if the module exposes functions called from onclick handlers

Adding database functionality: 1. Create table/view in Supabase 2. Create RPC function if complex logic needed 3. Add API call in api.js 4. Expose via module function 5. Wire up to UI

v5.0.1 File Changes Summary

File Change
index.html Analytics nav swapped to <a class="sidebar-nav-item sidebar-nav-external"> with external-link icon; cache-buster bumped
css/sidebar.css Added .sidebar-nav-external* styles, body.sidebar-mobile-open scroll lock, section max-height 500 → 1000px
css/layout.css Removed orphaned old tab bar CSS (.header, .nav-tabs, .tab-button, .hamburger-btn, and their media queries / print rules) — ~260 lines
js/sidebar.js .sidebar-nav-external skipped in click handler; openMobile/closeMobile toggle body scroll lock and manage focus; Escape closes mobile sidebar
js/config.js Bumped to v5.0.1

v5.0 File Changes Summary

File Change
index.html Removed old header + flat tab bar; added sidebar markup (brand, toggle, 4 section groups, badges, footer), backdrop, hamburger button; added agentReviewContent section; wrapped content in <main class="ops-main">
css/sidebar.css New file — sidebar layout, section collapse, nav items, badges, mobile overlay, print hiding
js/sidebar.js New file — navigation controller: tab switching, section collapse, sidebar collapse, mobile open/close, keyboard shortcuts, localStorage persistence
js/app.js Imported sidebar controller; replaced switchTab/initTabs with sidebar callback (_onTabSwitch); dropped flat-nav keyboard shortcuts; updated updateVersionDisplay to target #sidebarVersion
js/config.js Bumped to v5.0

v4.4 File Changes Summary

File Change
index.html Added Dog Photos tab button (data-tab="dogPhotos") after Testimonials, #dogPhotosContent section, #dog-photos-badge
js/app.js Added dogPhotos module import, registration in modules + tabModuleMap, window.opsPortal.dogPhotos binding
js/config.js Bumped to v4.4
js/modules/dog-photos.js New file - queue loader, signed URL resolver, moderation and revoke actions
css/dog-photos.css New file - thumbnail, consent pills, status badge styles
Edge Function dog-photo-upload New multipart upload endpoint writing to dog-photos bucket and submit_dog_photo RPC

v3.6 File Changes Summary

File Change
index.html Added conversation thread panel, editable subject field
js/app.js Updated support module integration for threading
js/config.js Bumped to v3.6
js/modules/support.js Added thread display, subject editing, send-support-email call
Edge Function send-support-email New unified email sending with threading

v3.5 File Changes Summary

File Change
index.html Added Agent Review tab + content section
js/app.js Added agentReview module import and registration
js/config.js Fixed syntax error, bumped to v3.5
js/modules/agent-review.js New file
js/modules/support.js Fixed order_number_label reference
css/agent-review.css New file

Version History

Version Date Changes
5.0.1 2026-04-18 Sidebar polish: Analytics as true external link (opens Metabase in new tab), body scroll lock + focus management on mobile open, Escape closes mobile sidebar, section max-height bumped to 1000px for future growth, orphaned old tab bar CSS cleanup (~260 lines).
5.0 2026-04-16 Sidebar navigation restructure (commit f887693). Replaced flat tab bar with grouped left sidebar — four sections: Operations, Product, Customer, Intelligence. Collapsible sidebar (240px ↔ 56px) and collapsible sections both persisted via localStorage. Mobile overlay pattern with hamburger trigger and backdrop dismissal. Analytics moved to external link. Testimonials moved under Customer. Keyboard shortcuts 1–9 for direct tab access. New files: css/sidebar.css, js/sidebar.js.
4.4 2026-04-15 Dog Photos moderation system (SOP-PHOTO-01 v1.0): new tab with moderation queue, channel-scoped consent pills (website/social/ads), four RPC functions, raw_ops.dog_photos table, public.v_dog_wall_display view, dog-photo-upload Edge Function, private dog-photos storage bucket. Config bumped to v4.4.
4.0 2026-04-13 System Health tab (13-tile dashboard, 7 exception queues, Worker Controls, Scheduled Tasks, Order Replay, Diagnostics, Manual Lab Entry modal with PASS+detected hard block); Compliance tab (5 recording forms); 6 new raw_ops.* tables; v_email_outbox view and mv_cron_last_runs matview; dashboard split into 4 parallel RPCs with progressive tile rendering; agent identity resolved from CF Access JWT. Governed by SOP-OPS-01 v1.2.
3.8 2026-02-17 Batch status terminology corrected (CLEARED→RELEASED, QA_FAIL→QUARANTINE), database schema updated to match live system, documentation aligned to portal v3.8
3.7 2026-02 Refund action buttons in Support Workstation
3.6 2026-01-20 Conversation thread panel, email threading (In-Reply-To/References), [Support] subject prefix, editable subject, ticket_messages table, send-support-email Edge Function
3.5 2026-01-19 Agent Review tab for shadow mode validation, bug fixes
3.4 2026-01-13 Documentation consolidation - merged v2.0 and v3.3 into comprehensive document
3.3 2025-12-10 Customer Search, Customer Profile, Ticket Assignment, Outbound Tickets
3.2 2025-12-10 Subscription actions, order history, internal notes, bidirectional Seal sync
3.1 2025-12-09 Support Workstation UI refinements
3.0 2025-12-04 Modular ES6 architecture, Support Workstation
2.1 2025-11-28 Auto-generated batch codes, dual ID system
2.0 2025-11-15 Pack Day, Fulfillment, Batch Creation, tab navigation, Protocol Raw branding
1.0 2025-11-04 Initial basic fulfillment interface

Document Owner: Protocol Raw Operations Team
Last Reviewed: April 18, 2026
Next Review: July 18, 2026
Version: 5.0.1
Status: Production Ready ✅


End of Protocol Raw Operations Portal Documentation v5.0.1