Skip to content

Conversation

@MANASMATHUR
Copy link
Contributor

@MANASMATHUR MANASMATHUR commented Jan 29, 2026

TinyFish - Logistics Intelligence Sentry
Live Demo: https://inventory-agent-three.vercel.app/

A comprehensive logistics intelligence platform that helps supply chain teams track port congestion, carrier advisories, and operational risks across multiple sources simultaneously. Uses the Discovery → Scouting → Synthesis pipeline pattern with parallel Mino browser agents to provide real-time, source-backed operational signals.

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Next.js app "logistics-sentry" with project configuration (package.json, next.config, jsconfig, Tailwind, PostCSS), global styles, and a README. Introduces server API routes (/api/logistics/risk-assessment, /api/agent/run, /api/pricing/run), TinyFish integrations and orchestration libraries, decision engine and pricing-intelligence modules, and numerous client React pages/components for dashboards, streaming UIs, and agent aesthetics. Adds utilities (cn), a toast hook, and exported route handlers and library functions. No changes to existing external public APIs.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Browser
    participant APIRoute as API Route<br/>(/api/logistics/risk-assessment)
    participant Agent as Logistics Agent<br/>(assessDelayRisk)
    participant TinyFish as TinyFish API<br/>(External Service)
    participant Synthesis as Result Synthesis<br/>(normalizeRiskResult)

    User->>APIRoute: POST (origin_port, carrier, mode)
    APIRoute->>APIRoute: Validate payload
    APIRoute->>Agent: assessDelayRisk(context)
    Agent->>Agent: Build discovery sources
    Agent->>TinyFish: Parallel source analyses (SSE)
    TinyFish-->>Agent: Streaming responses
    Agent->>Agent: Aggregate & synthesize findings
    Agent-->>APIRoute: Structured assessment
    APIRoute->>Synthesis: normalizeRiskResult(result)
    Synthesis-->>APIRoute: Normalized response
    APIRoute-->>User: JSON response (200/500)
Loading
sequenceDiagram
    participant User as User/Browser
    participant Page as Competitive Pricing<br/>Component
    participant APIRoute as API Route<br/>(/api/pricing/run)
    participant Pricing as Pricing Intelligence<br/>Agent
    participant TinyFish as TinyFish API<br/>(External Service)
    participant SSEParser as SSE Stream<br/>Parser

    User->>Page: Submit list of competitor URLs
    Page->>APIRoute: POST /api/pricing/run (urls)
    APIRoute->>APIRoute: Start parallel processing
    APIRoute->>Pricing: runPricingAnalysis(url, signal)
    Pricing->>TinyFish: POST goal (SSE)
    TinyFish-->>Pricing: SSE stream of events
    Pricing-->>APIRoute: Stream body (forwarded)
    APIRoute->>SSEParser: Parse SSE events, enrich results
    SSEParser-->>APIRoute: SSE-formatted data events
    APIRoute-->>Page: SSE stream (data/error/done)
    Page->>Page: Update UI per-event and per-URL
    Page-->>User: Real-time competitor results
Loading
🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance or quality. Add a description explaining the purpose, scope, and key features of the Logistics Sentry project.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main changeset: adding a new Logistics Sentry project that uses TinyFish Web Agents.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Fix all issues with AI agents
In `@logistics-sentry/src/app/api/pricing/run/route.js`:
- Around line 60-118: The ReadableStream lacks a cancel handler and timeouts
aren't cleared on errors; update the stream creation (ReadableStream
start/cancel) to track per-URL AbortController instances (e.g. a Map keyed by
url created inside start), pass each controller to runPricingAnalysis, move
clearTimeout(timeoutId) into a finally block in the per-url async function so
every path clears its timer, and implement the stream's cancel(controllerReason)
to iterate the Map and abort all in-flight controllers (and clear their timers)
so in-flight agent requests are aborted immediately when the client disconnects;
keep existing use of createSseParser, sendEvent, enrichPricingResult and
agentPromises while adding this cleanup logic.

In `@logistics-sentry/src/app/competitive-pricing/page.js`:
- Around line 64-68: The code calls response.body.getReader() without ensuring
response.body is non-null; add a defensive null-check after verifying
response.ok and before using getReader() — if response.body is null (e.g.,
204/empty response) throw a descriptive Error (or handle gracefully) so the
downstream TextDecoder/reader logic (the reader variable used to stream text and
the buffer variable) never attempts to call getReader() on null.
- Around line 308-312: Compute counts once and guard against division-by-zero:
in the component rendering the JSX snippet, assign const completedCount =
competitors.filter(c => c.status === "completed").length and const cheaperCount
= competitors.filter(c => c.standing === "CHEAPER").length, then only compute
Math.round((cheaperCount / completedCount) * 100) when completedCount > 0;
otherwise render a safe fallback (e.g., "--" or "0%"). Update the <p> that
currently references competitors.filter(... ) twice to use these variables so
you avoid repeated filtering and prevent Infinity or misleading values when
completedCount is zero.

In `@logistics-sentry/src/app/page.js`:
- Line 122: The toast call is passing an unsupported prop (variant:
"destructive") which the toast hook (useToast / toast) ignores; remove the
variant property from the toast invocation (the call to toast({ title: "Analysis
Error", description: err.message })) or, if you need styling variants, update
the toast implementation in use-toast.js to accept and handle a variant
field—ensure changes reference the toast() call and the use-toast.js handler so
the prop is either removed from page.js or properly consumed by the toast
provider.
- Line 97: The toast message currently interpolates origin but should show the
resolved value finalOrigin when a custom origin is active; update the toast call
(the line with toast({ title: "Initiating Network Scan", description: ... })) to
use `${finalOrigin}` for the description instead of `${origin}`, ensuring
finalOrigin is in scope where the toast is invoked and keeping the carrier
interpolation unchanged.

In `@logistics-sentry/src/components/ActionPanel.js`:
- Around line 1-9: Add the "use client" directive as the very first line of the
file to enable client-side interactivity for the ActionPanel component (so
onClick handlers like onProceed, onPause, onEscalate, onReset work), and remove
the unused MousePointer2 import from the lucide-react import list to avoid a
dead import; keep the remaining icon imports (CheckCircle2, PauseCircle,
AlertTriangle, RotateCcw) and ensure the directive appears above them and before
the ActionPanel export.
- Around line 24-51: The buttons in ActionPanel call callback props that may be
undefined (onProceed, onPause, onEscalate, onReset); update the component to
guard these invocations by either using optional chaining when calling them
(e.g., onProceed?.()) or by supplying default no-op functions for these props
(set defaults in the function signature or defaultProps) so clicking a button
never throws if a handler is not passed.

In `@logistics-sentry/src/components/InventoryAlert.js`:
- Around line 96-113: The buttons call the handlers onProceed, onPause, and
onEscalate directly which can throw if those props are undefined; update each
button's onClick to safely invoke the handler (e.g., use optional chaining like
onProceed?.(id) or ensure default no-op props) so clicking a button won't cause
a TypeError — apply the same change for onProceed, onPause, and onEscalate in
the InventoryAlert component.

In `@logistics-sentry/src/components/InventoryInput.js`:
- Around line 69-90: The setTimeout in handleConnectUrl schedules state updates
after 1200ms and lacks cleanup, causing potential state updates after unmount;
track the timer ID with a useRef (e.g., connectTimerRef), assign the return
value of setTimeout to that ref inside handleConnectUrl, and add a useEffect
cleanup that clears the timer via clearTimeout(connectTimerRef.current) when the
component unmounts so setIsConnecting, setActiveSource, and onInventoryLoaded
are never called after teardown.
- Around line 59-66: Add a maxSize: 50 * 1024 * 1024 (50MB) to the useDropzone
options and implement onDropRejected to show a toast notification for rejected
files so oversized uploads are prevented before FileReader.readAsText() is
called; update the useDropzone call that defines getRootProps, getInputProps,
isDragActive and onDrop to include maxSize and onDropRejected, and ensure the
onDrop handler skips reading files that were rejected (or rely on
onDropRejected) to avoid loading large files into memory.

In `@logistics-sentry/src/components/LiveStream.js`:
- Line 8: LiveStream can crash if the events prop is undefined; update the
component signature or destructuring to provide a safe default (e.g., set events
= [] in the LiveStream({ events, isRunning, currentPhase }) parameter or
immediately assign const safeEvents = events || []) so that calls to
events.some, events.map and events.length are always safe, and then use that
safe array (or the reassigned variable) throughout the component (referencing
the LiveStream function and its internal uses of events).

In `@logistics-sentry/src/components/MetricCard.js`:
- Line 33: The MetricCard component currently renders the passed-in Icon prop
unguarded (rendering <Icon ... />), which will crash if icon is undefined;
update the MetricCard render to either provide a default icon or conditionally
render the icon only when the icon prop exists (e.g., check the icon prop or
Icon symbol before using it) so that Icon is not invoked when undefined;
reference the MetricCard component and the Icon symbol in your change and ensure
the JSX uses a guard or a fallback default icon.

In `@logistics-sentry/src/lib/decision-engine.js`:
- Around line 5-8: The evaluate function computes drop using data.expected_stock
and will divide by zero if data.expected_stock is 0; update evaluate to first
guard against non-positive or falsy expected_stock (e.g., if data.expected_stock
== null or data.expected_stock <= 0) and return a safe default (such as false)
or handle the case explicitly before computing drop, then compute drop =
(data.expected_stock - data.current_stock) / data.expected_stock and compare to
0.5; reference the evaluate function, the drop variable, and
data.expected_stock/data.current_stock when making the change.
- Around line 32-39: The exported riskRules array is unused; either remove the
export to delete dead code or wire it into the evaluation path: update
evaluateRisk to iterate over riskRules (apply each rule's condition to the
agentOutput and return the rule's action when matched) so decision logic comes
from riskRules rather than hardcoded checks; ensure riskRules (and any predicate
keys) and evaluateRisk remain in the same module or are imported correctly and
add tests verifying a rule match returns the expected action and that the
existing confidence_score/recommended_action fallbacks still work.

In `@logistics-sentry/src/lib/logistics/agent.js`:
- Around line 284-289: In the congestion inference block that inspects s.summary
(the variable s) and adds to primaryCauses, fix the misspelled keyword "dweel"
to "dwell" so dwell-time mentions are matched (update the text.includes("dweel")
check to text.includes("dwell") — you can also add common variants like
"dwell-time" or "dwelltime" if desired) to ensure
primaryCauses.add("CONGESTION") is triggered correctly.

In `@logistics-sentry/src/lib/pricing-intelligence.js`:
- Around line 4-7: Add early guard clauses at the start of runPricingAnalysis to
validate TINYFISH_API_KEY and competitorUrl: check that TINYFISH_API_KEY (the
constant at top) is present and non-empty and that the competitorUrl argument is
provided and parses as a valid URL (use the URL constructor or a simple regex)
and throw/return a clear, specific error if either check fails so the function
fails fast before attempting the network call.
🧹 Nitpick comments (19)
logistics-sentry/package.json (2)

2-2: Package name doesn't match directory name.

The package is named "inventory-risk-agent" but resides in the logistics-sentry directory. Consider aligning the package name with the directory for consistency and to avoid confusion.

Suggested fix
-  "name": "inventory-risk-agent",
+  "name": "logistics-sentry",

15-17: Consider consistent version pinning strategy.

next is pinned to an exact version (14.2.3) while react and react-dom use caret ranges (^18). For better reproducibility, consider using consistent versioning across core dependencies. Next.js 14.2.3 is compatible with React 18.x, so this works but the inconsistency may cause confusion.

logistics-sentry/tailwind.config.js (2)

3-8: Potentially unnecessary content path.

Line 7 includes "./components/**/*.{js,ts,jsx,tsx,mdx}" at the root level, but based on the project structure, components appear to be in ./src/components/. If no root-level components/ directory exists, this path can be removed to keep the config clean.

Suggested fix
     content: [
         "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
         "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
         "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
-        "./components/**/*.{js,ts,jsx,tsx,mdx}",
     ],

11-48: Consider consistent color format.

The color palette mixes HSL notation (e.g., background, foreground, primary) with hex/rgba values (e.g., success, warning, destructive). While functional, using a consistent format improves maintainability. This is a minor stylistic consideration.

logistics-sentry/jsconfig.json (1)

25-29: Consider removing TypeScript-specific include.

The include array references "next-env.d.ts", which is a TypeScript declaration file. Since this project uses JavaScript (.js/.jsx files per the package.json and other configs), this entry is unnecessary. It won't cause issues but can be removed for clarity.

Suggested fix
     "include": [
-        "next-env.d.ts",
         "**/*.js",
         "**/*.jsx"
     ],
logistics-sentry/src/components/ActivityFeed.js (2)

1-7: Unused import: Clock is imported but never used.

The Clock icon is imported from lucide-react but is not referenced anywhere in the component.

🧹 Remove unused import
 import {
     CheckCircle2,
     PauseCircle,
     AlertCircle,
     RefreshCw,
-    Clock
 } from "lucide-react";

11-25: Consider adding fallback handling for unknown activity types.

If activity.type is not one of the predefined keys (proceed, pause, escalate, update, alert), the icon will be undefined and no styling will be applied. This could result in a confusing UI.

💡 Add default fallback
     const icons = {
         proceed: <CheckCircle2 className="h-4 w-4 text-success" />,
         pause: <PauseCircle className="h-4 w-4 text-warning" />,
         escalate: <AlertCircle className="h-4 w-4 text-destructive" />,
         update: <RefreshCw className="h-4 w-4 text-info" />,
         alert: <AlertCircle className="h-4 w-4 text-warning" />,
+        default: <AlertCircle className="h-4 w-4 text-muted-foreground" />,
     };

     const colors = {
         proceed: "border-success/20 bg-success/5",
         pause: "border-warning/20 bg-warning/5",
         escalate: "border-destructive/20 bg-destructive/5",
         update: "border-info/20 bg-info/5",
         alert: "border-warning/20 bg-warning/5",
+        default: "border-white/10 bg-white/5",
     };

Then use icons[activity.type] || icons.default and colors[activity.type] || colors.default.

logistics-sentry/src/components/AgentHeader.js (1)

29-33: Consider mobile navigation accessibility.

The navigation is hidden on screens smaller than lg. If this is intentional for a single link, it's fine, but consider adding a mobile-friendly alternative if more navigation items are expected.

logistics-sentry/README.md (1)

6-7: Video won't render with markdown image syntax.

The ![Demo Video](Demo%20Video.mp4) syntax is intended for images, not videos. Most markdown renderers won't display the video inline.

📹 Options to display the video

Option 1: Convert to GIF and use image syntax

-![Demo Video](Demo%20Video.mp4)
+![Demo Video](demo.gif)

Option 2: Link to the video file

-![Demo Video](Demo%20Video.mp4)
+[Watch Demo Video](Demo%20Video.mp4)

Option 3: Use HTML video tag (if renderer supports it)

-![Demo Video](Demo%20Video.mp4)
+<video src="Demo%20Video.mp4" controls width="100%"></video>
logistics-sentry/src/components/RiskAssessment.js (2)

63-65: Details button appears non-functional.

The button lacks an onClick handler or link behavior. Users might expect it to do something.

💡 Options to address

Option 1: Add onClick handler (if functionality is planned)

-<button className="text-[10px] font-bold text-primary flex items-center gap-0.5 hover:gap-1 transition-all">
+<button 
+    onClick={() => onDetailsClick?.(item)}
+    className="text-[10px] font-bold text-primary flex items-center gap-0.5 hover:gap-1 transition-all"
+>
     Details <ChevronRight className="h-3 w-3" />
 </button>

Option 2: Remove if not needed yet
Remove the button until the feature is implemented.


16-22: Consider extracting duplicated risk level logic.

The threshold logic (>75 = High, >50 = Med, else Low) is repeated across badge color, badge text, icon color, and progress bar color. Extracting this would improve maintainability.

♻️ Extract risk level helper
const getRiskLevel = (score) => {
    if (score > 75) return { level: "High Risk", variant: "destructive" };
    if (score > 50) return { level: "Med Risk", variant: "warning" };
    return { level: "Low Risk", variant: "success" };
};

// Usage in component:
const { level, variant } = getRiskLevel(item.riskScore);
// Then use `variant` for styling classes and `level` for text

Also applies to: 26-30, 43-47

logistics-sentry/src/hooks/use-toast.js (1)

2-2: Unused React import.

React is imported but not used in this file. The hook doesn't use any React APIs.

🧹 Remove unused import
 "use client";
-import * as React from "react";
logistics-sentry/src/app/globals.css (1)

45-95: Consider defining these colors in Tailwind config instead.

The manual utility classes (.text-success, .bg-success\/5, etc.) could be defined in tailwind.config.js under theme.extend.colors, which would automatically generate all the opacity variants and reduce CSS maintenance.

💡 Tailwind config approach

In tailwind.config.js:

theme: {
  extend: {
    colors: {
      success: '#10b981',
      warning: '#f59e0b',
      info: '#3b82f6',
      // destructive is already defined via CSS variable
    }
  }
}

This auto-generates text-success, bg-success/5, border-success/20, etc.

logistics-sentry/src/components/InventoryAlert.js (2)

2-9: Unused imports detected.

AlertTriangle and ArrowUpRight are imported but never used in this component.

🧹 Remove unused imports
 import {
-    AlertTriangle,
-    ArrowUpRight,
     Clock,
     Check,
     Pause,
     ShieldAlert
 } from "lucide-react";

26-30: severityColors is defined but never used.

The severityColors object is declared but the component uses inline ternary expressions for severity-based styling instead (lines 42-43, 51-52). Either remove this unused variable or refactor to use it for consistency and reduced duplication.

logistics-sentry/src/app/api/agent/run/route.js (2)

1-2: Unused import: evaluateRisk.

The evaluateRisk function is imported but never used in this route handler. Remove it to keep the code clean.

♻️ Proposed fix
 import { runAgent } from "../../../../lib/tinyfish";
-import { evaluateRisk } from "../../../../lib/decision-engine";

15-24: Consider adding a timeout for the runAgent call.

The similar /api/pricing/run route uses a 35-second timeout with AbortController. For consistency and to prevent indefinite hangs, consider adding timeout handling here as well.

♻️ Proposed fix with timeout
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 35000);

-        const stream = await runAgent(sku, intendedUpdate, contextUrl);
+        const stream = await runAgent(sku, intendedUpdate, contextUrl, { signal: controller.signal });

         // Pass through the stream from TinyFish to the client
         return new Response(stream, {
             headers: {
                 "Content-Type": "text/event-stream",
                 "Cache-Control": "no-cache",
                 "Connection": "keep-alive",
             },
         });

Note: The timeout should be cleared when the stream completes, which may require wrapping the stream.

logistics-sentry/src/lib/tinyfish.js (2)

3-4: Consider validating TINYFISH_API_KEY at module load.

If the API key is missing, both functions will make requests without proper authentication, likely resulting in cryptic 401/403 errors. An early validation provides clearer feedback during development.

♻️ Proposed validation
 const TINYFISH_API_URL = "https://tinyfish.ai/v1/automation/run-sse";
 const TINYFISH_API_KEY = process.env.TINYFISH_API_KEY;

+if (!TINYFISH_API_KEY) {
+    console.warn("[tinyfish] TINYFISH_API_KEY is not set. API calls will fail.");
+}

80-105: Consider extracting shared fetch logic.

Both runAgent and runGenericAgent share identical fetch, error handling, and stream return logic. Extracting a common helper would reduce duplication.

♻️ Optional refactor to reduce duplication
async function callTinyFishAPI(url, goal, options = {}) {
    const response = await fetch(TINYFISH_API_URL, {
        method: "POST",
        headers: {
            "X-API-Key": TINYFISH_API_KEY,
            "Content-Type": "application/json",
        },
        signal: options.signal,
        body: JSON.stringify({
            url: url,
            goal: goal,
            browser_profile: "stealth"
        }),
    });

    if (!response.ok) {
        throw new Error(`TinyFish API error: ${response.statusText}`);
    }

    return response.body;
}

// Then use in both functions:
// return callTinyFishAPI(targetUrl, goal, options);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@logistics-sentry/src/app/api/pricing/run/route.js`:
- Around line 7-75: The request accepts an unbounded urls array which spawns one
agent per entry; add defensive limits and validation: introduce a MAX_URLS
constant and immediately after extracting urls validate it's an array, trim each
entry to string, filter out empty values, and reject with 400 if urls.length ===
0 or urls.length > MAX_URLS; validate each entry using the URL constructor (or
equivalent) and return 400 listing invalid entries if any; finally replace the
unconditional parallel spawn in the ReadableStream start (the urls.map(...)
block that creates agentController and agentPromises and uses agentControllers
Map) with a bounded-concurrency approach (batching or a concurrency limiter) to
ensure at most N concurrent TinyFish calls and to still populate
agentControllers for active requests.

In `@logistics-sentry/src/components/InventoryInput.js`:
- Around line 20-56: The FileReader lacks an onerror handler and has redundant
readAsText branches; add reader.onerror to handle read failures by showing a
destructive toast (use the existing toast function) with a clear message,
optionally clear or leave unchanged setActiveSource and avoid calling
onInventoryLoaded on error, and ensure any caught errors still show a parse
error toast; also simplify the conditional at the end by removing duplicate
branches and calling reader.readAsText(file) once. Reference the existing
reader, reader.onload, reader.onerror, setActiveSource, onInventoryLoaded, and
toast symbols when making the changes.

In `@logistics-sentry/src/lib/decision-engine.js`:
- Around line 3-9: evaluateRisk currently dereferences agentOutput without
checking for null/undefined, which can crash; add a guard at the top of the
evaluateRisk function (or use a default parameter) to handle missing input and
return a safe default (e.g., "PAUSE" or "ESCALATE" per business rule) before
accessing agentOutput.confidence_score or agentOutput.recommended_action; update
references inside evaluateRisk so all dereferences are safe when agentOutput is
absent.

In `@logistics-sentry/src/lib/logistics/agent.js`:
- Around line 176-177: Fix the typo in the agent prompt string "Detailed finding
with numbers/quotes if avaiable" by changing "avaiable" to "available" so the
value becomes "Detailed finding with numbers/quotes if available"; locate the
exact string (the "summary" prompt value) in
logistics-sentry/src/lib/logistics/agent.js and update it accordingly.
- Around line 253-289: The code assumes s.summary is always a string; guard
against missing/non-string summaries by normalizing summary into a local safe
string (e.g., const summary = typeof s.summary === 'string' ? s.summary : '')
before using it for formattedSignal and calling toLowerCase(); use that safe
summary when setting formattedSignal and when computing text for cause inference
(only run the keyword checks if summary is non-empty), and ensure date/default
handling still uses original s.summary where appropriate (e.g., display fallback
like "No summary"). Reference symbols: s.summary, formattedSignal, signals.push,
primaryCauses.add, and the block that calls s.summary.toLowerCase().
🧹 Nitpick comments (6)
logistics-sentry/src/components/MetricCard.js (2)

38-46: Consider defensive check for trend.value.

If trend is truthy but malformed (e.g., trend.value is undefined or null), this will render "undefined%" or "null%". Consider adding a fallback or stricter guard.

🛡️ Proposed defensive fix
                 {trend && (
                     <div className={cn(
                         "flex items-center gap-0.5 text-[9px] font-black px-1.5 py-0.5 rounded-full border shadow-sm",
                         trend.isPositive ? "text-success bg-success/10 border-success/20" : "text-destructive bg-destructive/10 border-destructive/20"
                     )}>
                         {trend.isPositive ? <TrendingUp className="h-2.5 w-2.5" /> : <TrendingDown className="h-2.5 w-2.5" />}
-                        {trend.value}%
+                        {trend.value ?? 0}%
                     </div>
                 )}

37-44: Very small font sizes may impact accessibility.

The text-[9px] and text-[10px] classes result in extremely small text that could be difficult to read for users with visual impairments. WCAG recommends a minimum of 12px for body text. Consider whether these sizes are intentional for the design or could be increased.

logistics-sentry/src/components/LiveStream.js (2)

62-67: Avoid using array index as key with AnimatePresence.

Using key={i} with framer-motion's AnimatePresence can cause incorrect enter/exit animations when events are added or removed mid-stream. If an event is prepended or removed, the indices shift, and framer-motion may animate the wrong items or skip exit animations entirely.

Consider adding a unique identifier to each event object (e.g., a UUID or incrementing ID when events are created) and use that as the key.

♻️ Suggested approach
-                    {events.map((event, i) => (
-                        <motion.div
-                            key={i}
+                    {events.map((event) => (
+                        <motion.div
+                            key={event.id}

This requires ensuring each event has a unique id property when created upstream.


11-16: Consider extracting phases as a module-level constant.

The phases array is static and recreated on every render. Moving it outside the component avoids unnecessary allocations and makes it reusable if other components need the same phase definitions.

♻️ Suggested change
+const PHASES = [
+    { id: "SURFACE_SCAN", label: "Dashboard Scan" },
+    { id: "SOURCE_VERIFICATION", label: "Audit Logs" },
+    { id: "BUSINESS_CONTEXT", label: "Sales Analysis" },
+    { id: "SYNTHESIS", label: "Final Synthesis" }
+];
+
 export function LiveStream({ events = [], isRunning, currentPhase }) {
     const scrollRef = useRef(null);
-
-    const phases = [
-        { id: "SURFACE_SCAN", label: "Dashboard Scan" },
-        { id: "SOURCE_VERIFICATION", label: "Audit Logs" },
-        { id: "BUSINESS_CONTEXT", label: "Sales Analysis" },
-        { id: "SYNTHESIS", label: "Final Synthesis" }
-    ];

Then reference PHASES instead of phases in the JSX.

logistics-sentry/src/components/InventoryInput.js (1)

104-124: Consider adding ARIA attributes for tab accessibility.

The mode toggle buttons function as tabs but lack ARIA attributes (role="tab", aria-selected, role="tablist" on container). This can improve screen reader navigation.

Suggested enhancement
-            <div className="flex items-center border-b border-primary/10">
+            <div className="flex items-center border-b border-primary/10" role="tablist">
                 <button
                     onClick={() => setMode("file")}
+                    role="tab"
+                    aria-selected={mode === "file"}
                     className={cn(
                         "flex-1 py-3 text-xs font-bold uppercase tracking-widest transition-all flex items-center justify-center gap-2",
                         mode === "file" ? "bg-primary/5 text-primary" : "text-muted-foreground hover:bg-primary/5"
                     )}
                 >
                     <UploadCloud className="h-4 w-4" /> Upload Manifest
                 </button>
                 <div className="w-[1px] h-full bg-primary/10" />
                 <button
                     onClick={() => setMode("url")}
+                    role="tab"
+                    aria-selected={mode === "url"}
                     className={cn(
                         "flex-1 py-3 text-xs font-bold uppercase tracking-widest transition-all flex items-center justify-center gap-2",
                         mode === "url" ? "bg-primary/5 text-primary" : "text-muted-foreground hover:bg-primary/5"
                     )}
                 >
                     <LinkIcon className="h-4 w-4" /> Connect URL
                 </button>
             </div>
logistics-sentry/src/app/competitive-pricing/page.js (1)

2-33: Abort the streaming fetch on unmount to prevent orphaned work.

If the user navigates away mid‑stream, the fetch keeps running and state updates may fire after unmount. Use an AbortController stored in a ref and cancel it in a cleanup effect; optionally ignore AbortError.

♻️ Suggested refactor
-"use client";
-import { useState, useEffect } from "react";
+"use client";
+import { useState, useEffect, useRef } from "react";
@@
     const [competitors, setCompetitors] = useState([]);
     const [logs, setLogs] = useState([]);
     const [isMounted, setIsMounted] = useState(false);
+    const abortRef = useRef(null);
 
     useEffect(() => {
         setIsMounted(true);
+        return () => abortRef.current?.abort();
     }, []);
@@
-        try {
+        try {
+            const controller = new AbortController();
+            abortRef.current = controller;
             const response = await fetch("/api/pricing/run", {
                 method: "POST",
                 headers: { "Content-Type": "application/json" },
-                body: JSON.stringify({ urls: urlList }),
+                body: JSON.stringify({ urls: urlList }),
+                signal: controller.signal,
             });
@@
-        } catch (err) {
+        } catch (err) {
+            if (err?.name === "AbortError") return;
             setIsRunning(false);
             toast({ title: "Analysis Failed", description: err.message });
         }

Also applies to: 35-137

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@logistics-sentry/src/lib/logistics/agent.js`:
- Around line 133-213: The SSE parser (createSseParser) must normalize CRLF and
not drop trailing buffered events: update createSseParser to replace all "\r\n"
with "\n" (or otherwise normalize line endings) before splitting, split on
"\n\n" as now, and expose or implement a flush path so any remaining buffer is
parsed when the stream ends (so the parse instance used in analyzeSource will
still pick up data.final_result). Ensure you still filter lines by "data: " and
JSON.parse into onEvent (preserve existing checks) and that analyzeSource's
parse variable will receive the final chunk via the flush at reader.done so
finalResult is not lost.
- Around line 260-292: The code currently uses s.severity directly which can be
undefined and causes undercounts; normalize it first (e.g., const severity =
s.severity || "UNKNOWN") and then use that variable everywhere: when pushing
into signals (use severity), when adjusting riskScore (check severity ===
"HIGH"/"MEDIUM"/"LOW"), when incrementing severityCounts (ensure
severityCounts[severity] exists or initialize to 0 before increment), and when
deciding non-LOW logic for primaryCauses; also ensure severityCounts has an
"UNKNOWN" bucket initialized so the UI/stats remain stable.
🧹 Nitpick comments (3)
logistics-sentry/src/app/competitive-pricing/page.js (3)

3-17: Remove unused imports.

Plus, X, and LayoutGrid are imported but not used anywhere in this component.

🧹 Proposed fix
 import {
     Search,
     Globe,
     TrendingUp,
     Shield,
     Zap,
     BarChart3,
     AlertCircle,
     CheckCircle2,
     Loader2,
-    Plus,
-    X,
-    LayoutGrid,
     Table as TableIcon
 } from "lucide-react";

163-172: Remove development comments.

These lines contain verbose development notes and implementation discussions that should be cleaned up before merging.

🧹 Proposed fix
         } finally {
             setIsRunning(false);
         }
-
-        // Cleanup function for this specific run if component unmounts; 
-        // Note: Ideally we attach this to a ref in useEffect, but for this event handler scope, 
-        // we can just return the abort function if we were binding it to a state.
-        // Since this is an event handler, we should actually store the controller in a ref to cancel on unmount.
-        // For now, I will add the ref logic in a separate step or just assume this is "good enough" for the scope 
-        // but the PR feedback specifically asked for "abort on unmount".
-        // I will add the ref logic in the NEXT tool call to be safe, or just leave it here if I can edit the whole component.
-        // Actually, I can't easily edit the whole component to add a ref without reading more lines. 
-        // I'll stick to fixing the syntax error first.
     };

260-262: Consider using URL as key instead of array index.

Using array index as key can cause animation and state issues if items are reordered. Since each competitor has a unique URL, it would be a more stable key.

♻️ Proposed fix
-{competitors.map((c, idx) => (
+{competitors.map((c) => (
     <motion.div
-        key={idx}
+        key={c.url}
         initial={{ opacity: 0, y: 10 }}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@logistics-sentry/src/lib/logistics/agent.js`:
- Around line 438-441: The function assessDelayRisk destructures properties from
context without guarding against undefined; change its signature or initial
handling so context defaults to an empty object (e.g., default parameter or an
early fallback) before destructuring origin_port, carrier, and mode to avoid a
runtime crash when callers pass undefined; update the assessDelayRisk function
definition to accept context = {} or add a safe assignment at the top of the
function.
- Around line 271-281: The METRIC/QUOTE branches currently overwrite the
fallback by using safeSummary directly; update the branches that set
formattedSignal (the lines handling s.category === "METRIC" and s.category ===
"QUOTE") to use the existing fallback when safeSummary is empty (e.g., use
safeSummary || "No summary available") so formattedSignal remains "No summary
available" when summary is missing while still applying the METRIC/QUOTE
wrappers.
🧹 Nitpick comments (1)
logistics-sentry/src/lib/logistics/agent.js (1)

301-365: Avoid lexicographic date ordering for latestSignalDate.

signalDates.sort() is string-based and can misorder or accept invalid dates. Consider timestamp-based ordering so “latest” reflects actual chronology.

Suggested refactor
-    const latestSignalDate = signalDates.length > 0 ? signalDates.sort().slice(-1)[0] : null;
+    const latestSignalDate = signalDates.length > 0
+        ? signalDates
+            .map(d => ({ d, ts: Date.parse(d) }))
+            .filter(x => Number.isFinite(x.ts))
+            .sort((a, b) => a.ts - b.ts)
+            .slice(-1)[0]?.d || null
+        : null;

Copy link
Contributor

@uttambharadwaj uttambharadwaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty cool! i think it can show more detail about what is happening bts!

@uttambharadwaj uttambharadwaj merged commit ced0544 into tinyfish-io:main Feb 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants