Skip to content

Add analytics tab for Risks and Model Inventory#2641

Merged
gorkem-bwl merged 8 commits intodevelopfrom
hp-nov-7-add-analytics
Nov 13, 2025
Merged

Add analytics tab for Risks and Model Inventory#2641
gorkem-bwl merged 8 commits intodevelopfrom
hp-nov-7-add-analytics

Conversation

@HarshP4585
Copy link
Copy Markdown
Collaborator

@HarshP4585 HarshP4585 commented Nov 7, 2025

Describe your changes

  • Added analytics tab for Risk management and Model inventory

Write your issue number after "Fixes "

Fixes #2673

Please ensure all items are checked off before requesting a review:

  • I deployed the code locally.
  • I have performed a self-review of my code.
  • I have included the issue # in the PR.
  • I have labelled the PR correctly.
  • The issue I am working on is assigned to me.
  • I have avoided using hardcoded values to ensure scalability and maintain consistency across the application.
  • I have ensured that font sizes, color choices, and other UI elements are referenced from the theme.
  • My pull request is focused and addresses a single, specific feature.
  • If there are UI changes, I have attached a screenshot or video to this PR.

@HarshP4585 HarshP4585 requested a review from gorkem-bwl November 7, 2025 17:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 7, 2025

Warning

Rate limit exceeded

@gorkem-bwl has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 20 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5f67ea6 and d51a5ce.

📒 Files selected for processing (2)
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1 hunks)

Walkthrough

Adds full historical analytics for models and risks: client API wrappers, UI drawer and charts, server controllers/routes, Sequelize models/interfaces, per-tenant migrations, history utilities with interpolation and snapshot recording, and hooks to record snapshots during create/update/delete flows.

Changes

Cohort / File(s) Summary
Client Repositories
Clients/src/application/repository/modelInventoryHistory.repository.ts, Clients/src/application/repository/riskHistory.repository.ts
New API wrappers: timeseries fetch, current-parameter counts, and manual snapshot creation. Use apiServices, include try/catch logging and rethrow behavior.
Client Charts
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx, Clients/src/presentation/components/Charts/RiskHistoryChart.tsx
New React line-chart components with timeframe toggles: fetch timeseries, handle loading/error/empty states, build multi-series, dynamic y-scaling and legend.
Analytics UI
Clients/src/presentation/components/AnalyticsDrawer/index.tsx
New AnalyticsDrawer component rendering parameter picker and conditionally showing model or risk charts; manages selection and open/close props.
Client Pages
Clients/src/presentation/pages/ModelInventory/index.tsx, Clients/src/presentation/pages/RiskManagement/index.tsx
Add Analytics (TrendingUp) button and integrate AnalyticsDrawer; manage open state and pass available/default parameters.
Form Refactor
Clients/src/presentation/components/AddNewRiskForm/index.tsx
Refactored change detection: generic getChangedFields, use risk_*/mitigation_* prefixed keys, separate risk/mitigation change sets, and build update payloads from prefixed fields.
Server Controllers
Servers/controllers/modelInventoryHistory.ctrl.ts, Servers/controllers/riskHistory.ctrl.ts
New controllers: getTimeseries, getCurrentCounts, createSnapshot. Validate parameter/timeframe/dates, call history utils, structured logging and HTTP responses.
Server Routes
Servers/routes/modelInventoryHistory.route.ts, Servers/routes/riskHistory.route.ts
New JWT-protected routers exposing GET /timeseries, GET /current-counts, POST /snapshot.
Database Models & Interfaces
Servers/domain.layer/models/.../modelInventoryHistory.model.ts, .../riskHistory.model.ts, Servers/domain.layer/interfaces/i.modelInventoryHistory.ts, Servers/domain.layer/interfaces/i.riskHistory.ts
New Sequelize-Typescript models and TypeScript interfaces describing history snapshots (id, parameter, snapshot_data JSONB, recorded_at, triggered_by_user_id, change_description, created_at) and BelongsTo user relations.
Database Migrations & Setup
Servers/database/migrations/20251105000000-create-model-inventory-history-table.js, Servers/database/migrations/20251107000000-create-risk-history-table.js, Servers/database/db.ts, Servers/scripts/createNewTenant.ts
Per-tenant migrations to create model_inventory_history and risk_history tables with indexes (parameter, recorded_at DESC, composite), register models in DB init, and create tenant tables during tenant setup.
History Utilities
Servers/utils/history/modelInventoryHistory.utils.ts, Servers/utils/history/riskHistory.utils.ts
Utilities to record snapshots, compute current parameter counts, fetch timeseries, interpolate missing points, get latest/at-time snapshots, and helpers shouldRecordSnapshot / recordSnapshotIfChanged.
Existing Utils Integration
Servers/utils/modelInventory.utils.ts, Servers/utils/risk.utils.ts
Call recordSnapshotIfChanged after create/update/delete flows for relevant parameters; errors are logged and non-blocking.
Server Index
Servers/index.ts
Register new route modules: /api/modelInventoryHistory and /api/riskHistory.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Page as UI Page
    participant Drawer as AnalyticsDrawer
    participant Chart as HistoryChart
    participant Repo as Client Repo
    participant API as Server Controller
    participant DB as Database

    User->>Page: Click Analytics button
    Page->>Drawer: open()
    Drawer->>Chart: init(selectedParameter, timeframe)
    Chart->>Repo: getTimeseries(parameter, timeframe)
    Repo->>API: GET /api/{model|risk}History/timeseries?parameter=...&timeframe=...
    API->>DB: query snapshots & interpolation
    DB-->>API: timeseries rows
    API-->>Repo: timeseries payload
    Repo-->>Chart: data
    Chart->>Chart: map snapshots → series
    Chart-->>Drawer: render(chart)
Loading
sequenceDiagram
    participant App as CRUD Operation
    participant Util as entity util (modelInventory/risk)
    participant History as history.utils
    participant DB as Database

    App->>Util: create/update/delete(...)
    Util->>History: recordSnapshotIfChanged(parameter, tenant, userId, tx)
    History->>DB: getLatestSnapshot(parameter)
    DB-->>History: latestSnapshot/null
    History->>History: compare currentCounts vs latest
    alt changed
        History->>DB: INSERT new snapshot
        DB-->>History: new record
        History-->>Util: snapshot created
    else unchanged
        History-->>Util: no-op
    end
    Util-->>App: complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • Servers/utils/history/* — interpolation logic, time arithmetic, timezone handling, and tenant schema queries.
    • Servers/controllers/* — input validation (timeframe vs custom dates), error/status mapping.
    • Database migrations & tenant setup — per-tenant table creation, index definitions, transaction safety, and down migration symmetry.
    • Integration points (Servers/utils/modelInventory.utils.ts, Servers/utils/risk.utils.ts) — correct transaction propagation and non-blocking failure behavior.
    • Client charts — correctness of data transformation into multi-series and axis scaling.

Poem

🐰 I hopped through snapshots, tidy and bright,
Counting statuses by day and by night,
Charts that prance with timelines in view,
Snapshots recorded — old colors made new,
Hop, hop — analytics take flight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides the issue number (#2673) and a brief summary of changes, but multiple required checklist items remain unchecked (deploy locally, self-review, include issue #, label PR, etc.), indicating incomplete pre-submission preparation. Complete all checklist items by verifying local deployment, performing self-review, confirming issue assignment, applying appropriate labels, ensuring theme consistency, and attaching UI screenshots/videos as applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add analytics tab for Risks and Model Inventory' directly and clearly summarizes the primary change in the changeset—adding analytics functionality to both risk management and model inventory sections.
Linked Issues check ✅ Passed The PR implements the core requirements from issue #2673: adds historical analytics functionality for both Risks and Model Inventory sections, enabling historical analytics data viewing as specified.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #2673. Backend infrastructure (models, migrations, controllers, utilities) and frontend components (charts, drawers, page integrations) collectively implement the analytics feature consistently across both risk and model inventory domains without introducing unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.

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.


return result[0];
} catch (error) {
console.error(`Error recording history snapshot for parameter ${parameter}:`, error);

Check failure

Code scanning / CodeQL

Use of externally-controlled format string High

Format string depends on a
user-provided value
.

Copilot Autofix

AI 5 months ago

To eliminate the "Use of externally-controlled format string" risk, we should avoid embedding user-controlled input inside a template string or as part of the format string for logging. Instead, pass a constant format string with a %s substitution, and provide the untrusted data as a separate argument. Specifically:

  • On line 43 of Servers/utils/history/modelInventoryHistory.utils.ts, replace:
    console.error(`Error recording history snapshot for parameter ${parameter}:`, error);
    with:
    console.error("Error recording history snapshot for parameter %s:", parameter, error);

This ensures that any format specifiers in parameter will not be interpreted as part of the format string, regardless of the runtime environment’s behavior.

No new imports or function definitions are required; this is a simple fix confined to the logging line.


Suggested changeset 1
Servers/utils/history/modelInventoryHistory.utils.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Servers/utils/history/modelInventoryHistory.utils.ts b/Servers/utils/history/modelInventoryHistory.utils.ts
--- a/Servers/utils/history/modelInventoryHistory.utils.ts
+++ b/Servers/utils/history/modelInventoryHistory.utils.ts
@@ -40,7 +40,7 @@
 
     return result[0];
   } catch (error) {
-    console.error(`Error recording history snapshot for parameter ${parameter}:`, error);
+    console.error("Error recording history snapshot for parameter %s:", parameter, error);
     throw error;
   }
 }
EOF
@@ -40,7 +40,7 @@

return result[0];
} catch (error) {
console.error(`Error recording history snapshot for parameter ${parameter}:`, error);
console.error("Error recording history snapshot for parameter %s:", parameter, error);
throw error;
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +105 to +107
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".model_inventories
GROUP BY ${parameter}`,

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query string depends on a
user-provided value
.
This query string depends on a
user-provided value
.

Copilot Autofix

AI 5 months ago

To fix this problem, the code should only allow a safe, expected set of parameter names to be used as SQL column names. This means introducing a whitelist/checklist of allowed parameter/column names (such as 'status', 'security_assessment', or other known, safe strings), and refusing any value that does not match these. For generic handling (the else block), instead of directly interpolating parameter into the SQL string, the code should check that it is an exact match of a known column name. If not, it should throw an error or return a 400 status. Implementation requires:

  • Defining a list of allowed column names near the top of the utils file.
  • In getCurrentParameterCounts, before using ${parameter} in the query, check if it is in the allowed list; if not, throw an error.
  • No external libraries are strictly required, but you may define the allowed list where convenient.
    Edit only in the shown Servers/utils/history/modelInventoryHistory.utils.ts snippet.

Suggested changeset 1
Servers/utils/history/modelInventoryHistory.utils.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Servers/utils/history/modelInventoryHistory.utils.ts b/Servers/utils/history/modelInventoryHistory.utils.ts
--- a/Servers/utils/history/modelInventoryHistory.utils.ts
+++ b/Servers/utils/history/modelInventoryHistory.utils.ts
@@ -3,6 +3,19 @@
 import { ModelInventoryStatus } from "../../domain.layer/enums/model-inventory-status.enum";
 import { Transaction, QueryTypes } from "sequelize";
 
+
+// Allowed parameter/column names for getCurrentParameterCounts
+const ALLOWED_PARAMETERS = [
+  "status",
+  "security_assessment",
+  // Add other allowed model_inventories columns below as needed
+  "model_type",
+  "owner",
+  "created_at",
+  "updated_at",
+  // ...etc
+];
+
 /**
  * Record a snapshot of parameter counts in history
  */
@@ -100,11 +113,15 @@
         }
       });
     } else {
-      // Generic handling for other parameters
+      // Only allow known, allowed column names for generic parameters
+      if (!ALLOWED_PARAMETERS.includes(parameter)) {
+        throw new Error(`Invalid or disallowed parameter for model inventory counts: ${parameter}`);
+      }
+      // Safe to use: parameter is whitelisted as a column name
       const paramCounts = await sequelize.query(
-        `SELECT ${parameter}, COUNT(*) as count
+        `SELECT "${parameter}", COUNT(*) as count
          FROM "${tenant}".model_inventories
-         GROUP BY ${parameter}`,
+         GROUP BY "${parameter}"`,
         {
           type: QueryTypes.SELECT,
           transaction,
EOF
@@ -3,6 +3,19 @@
import { ModelInventoryStatus } from "../../domain.layer/enums/model-inventory-status.enum";
import { Transaction, QueryTypes } from "sequelize";


// Allowed parameter/column names for getCurrentParameterCounts
const ALLOWED_PARAMETERS = [
"status",
"security_assessment",
// Add other allowed model_inventories columns below as needed
"model_type",
"owner",
"created_at",
"updated_at",
// ...etc
];

/**
* Record a snapshot of parameter counts in history
*/
@@ -100,11 +113,15 @@
}
});
} else {
// Generic handling for other parameters
// Only allow known, allowed column names for generic parameters
if (!ALLOWED_PARAMETERS.includes(parameter)) {
throw new Error(`Invalid or disallowed parameter for model inventory counts: ${parameter}`);
}
// Safe to use: parameter is whitelisted as a column name
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
`SELECT "${parameter}", COUNT(*) as count
FROM "${tenant}".model_inventories
GROUP BY ${parameter}`,
GROUP BY "${parameter}"`,
{
type: QueryTypes.SELECT,
transaction,
Copilot is powered by AI and may make mistakes. Always verify output.

return counts;
} catch (error) {
console.error(`Error getting current parameter counts for ${parameter}:`, error);

Check failure

Code scanning / CodeQL

Use of externally-controlled format string High

Format string depends on a
user-provided value
.
Format string depends on a
user-provided value
.

Copilot Autofix

AI 5 months ago

To fix this issue, we should avoid directly injecting untrusted user input into the format string within a template literal. Instead, we should use a static format string and pass the untrusted value as an argument to the logging function, e.g. passing %s as a placeholder and providing parameter as the corresponding value.

  • In file Servers/utils/history/modelInventoryHistory.utils.ts, specifically at line 122, replace:
    console.error(`Error getting current parameter counts for ${parameter}:`, error);
    with:
    console.error("Error getting current parameter counts for %s:", parameter, error);

This ensures that the value for parameter is not used to control the logging format and is safely rendered as a string.

No additional imports or dependencies are required, as these are built-in JavaScript patterns.


Suggested changeset 1
Servers/utils/history/modelInventoryHistory.utils.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Servers/utils/history/modelInventoryHistory.utils.ts b/Servers/utils/history/modelInventoryHistory.utils.ts
--- a/Servers/utils/history/modelInventoryHistory.utils.ts
+++ b/Servers/utils/history/modelInventoryHistory.utils.ts
@@ -119,7 +119,7 @@
 
     return counts;
   } catch (error) {
-    console.error(`Error getting current parameter counts for ${parameter}:`, error);
+    console.error("Error getting current parameter counts for %s:", parameter, error);
     throw error;
   }
 }
EOF
@@ -119,7 +119,7 @@

return counts;
} catch (error) {
console.error(`Error getting current parameter counts for ${parameter}:`, error);
console.error("Error getting current parameter counts for %s:", parameter, error);
throw error;
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown
Contributor

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (1)

431-455: Fix TypeScript error: Make getChangedFields generic.

The function signature expects RiskFormValues & MitigationFormValues but at lines 635-636 it's called with separate RiskFormValues and MitigationFormValues types, causing the pipeline failure.

Apply this diff to make the function generic:

-  const getChangedFields = useCallback((
-    original: RiskFormValues & MitigationFormValues,
-    current: RiskFormValues & MitigationFormValues
-  ) => {
+  const getChangedFields = useCallback(<T extends Record<string, any>>(
+    original: T,
+    current: T
+  ): Partial<T> => {
     const changedFields: any = {};
 
     // Check each field for changes
     Object.keys(current).forEach((key) => {
-      const originalValue = original[key as keyof (RiskFormValues & MitigationFormValues)];
-      const currentValue = current[key as keyof (RiskFormValues & MitigationFormValues)];
+      const originalValue = original[key as keyof T];
+      const currentValue = current[key as keyof T];
 
       // For arrays, do deep comparison
       if (Array.isArray(originalValue) && Array.isArray(currentValue)) {
🧹 Nitpick comments (11)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)

524-554: Well-structured field mapping with clear prefixes.

The prefixing approach effectively distinguishes risk vs mitigation fields and the mapping is comprehensive.

Consider extracting the field mapping into a typed constant at the module level for better maintainability and type safety:

const FIELD_MAPPING = {
  risk: {
    riskName: 'risk_name',
    actionOwner: 'risk_owner',
    // ... other risk fields
  },
  mitigation: {
    mitigationStatus: 'mitigation_status',
    // ... other mitigation fields
  }
} as const;

634-664: Consider stronger typing for changedFields object.

The logic for computing changed fields separately and combining with prefixes is sound, and the boolean flags correctly use prefixed keys.

Based on learnings, avoid using any type when a more specific type is available. Consider:

-        const changedFields: any = {};
+        const changedFields: Record<string, any> = {};

Or better yet, create a proper type:

type PrefixedRiskFields = { [K in keyof RiskFormValues as `risk_${string & K}`]: RiskFormValues[K] };
type PrefixedMitigationFields = { [K in keyof MitigationFormValues as `mitigation_${string & K}`]: MitigationFormValues[K] };
type ChangedFields = Partial<PrefixedRiskFields & PrefixedMitigationFields>;

const changedFields: ChangedFields = {};
Clients/src/presentation/pages/RiskManagement/index.tsx (1)

390-405: Use theme references instead of hardcoded color values.

The Analytics button uses hardcoded color values (#7F56D9, #6941C6) which should be replaced with theme references for consistency and maintainability.

Apply this diff to use theme colors:

             <div data-joyride-id="analytics-button">
               <CustomizableButton
                 variant="contained"
                 text="Analytics"
                 sx={{
-                  backgroundColor: "#7F56D9",
-                  border: "1px solid #7F56D9",
+                  backgroundColor: "primary.main",
+                  border: "1px solid",
+                  borderColor: "primary.main",
                   gap: 2,
                   "&:hover": {
-                    backgroundColor: "#6941C6",
+                    backgroundColor: "primary.dark",
                   },
                 }}
                 onClick={() => setIsAnalyticsDrawerOpen(true)}
                 icon={<TrendingUp size={16} />}
               />
             </div>
Servers/scripts/createNewTenant.ts (1)

977-993: Consider adding indexes for query performance.

The history tables are properly structured with appropriate constraints. However, for better query performance on time-series data retrieval, consider adding indexes on frequently queried columns.

Add indexes after table creation:

await sequelize.query(`CREATE TABLE "${tenantHash}".risk_history (
  id SERIAL PRIMARY KEY,
  parameter VARCHAR(255) NOT NULL,
  snapshot_data JSONB NOT NULL DEFAULT '{}',
  recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
  triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
  created_at TIMESTAMP DEFAULT NOW()
);`, { transaction });

// Add indexes for performance
await sequelize.query(`
  CREATE INDEX idx_risk_history_parameter ON "${tenantHash}".risk_history(parameter);
  CREATE INDEX idx_risk_history_recorded_at ON "${tenantHash}".risk_history(recorded_at DESC);
`, { transaction });

await sequelize.query(`CREATE TABLE "${tenantHash}".model_inventory_history (
  id SERIAL PRIMARY KEY,
  parameter VARCHAR(255) NOT NULL,
  snapshot_data JSONB NOT NULL DEFAULT '{}',
  recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
  triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
  created_at TIMESTAMP DEFAULT NOW()
);`, { transaction });

// Add indexes for performance
await sequelize.query(`
  CREATE INDEX idx_model_inventory_history_parameter ON "${tenantHash}".model_inventory_history(parameter);
  CREATE INDEX idx_model_inventory_history_recorded_at ON "${tenantHash}".model_inventory_history(recorded_at DESC);
`, { transaction });
Clients/src/presentation/pages/ModelInventory/index.tsx (2)

877-896: Use theme references for button styling.

The Analytics button reuses addNewModelButtonStyle which contains hardcoded color values (#13715B). Consider creating a theme-based style or using theme references directly.

Replace the hardcoded style:

               <CustomizableButton
                 variant="contained"
                 onClick={() => setIsAnalyticsDrawerOpen(true)}
-                sx={addNewModelButtonStyle}
+                sx={{
+                  backgroundColor: "secondary.main",
+                  border: "1px solid",
+                  borderColor: "secondary.main",
+                }}
                 icon={<TrendingUp size={16} />}
                 text="Analytics"
               />

1009-1021: Consider adding more analytics parameters for comprehensive insights.

The analytics drawer currently only tracks "status" parameter. Consider adding additional parameters like provider, security_assessment status, or other relevant model inventory attributes to provide more comprehensive analytics.

Example:

       <AnalyticsDrawer
         open={isAnalyticsDrawerOpen}
         onClose={() => setIsAnalyticsDrawerOpen(false)}
         title="Analytics & Trends"
         description="Track your model inventory history over time"
         entityName="Model"
+        chartType="modelInventory"
         availableParameters={[
           { value: "status", label: "Status" },
-          // Add more parameters here as needed
+          { value: "provider", label: "Provider" },
+          { value: "security_assessment", label: "Security Assessment" },
         ]}
         defaultParameter="status"
       />
Servers/utils/modelInventory.utils.ts (1)

255-262: Consider tracking additional parameters.

Currently only the status parameter is tracked for model inventory history. Based on the interface comments in i.modelInventoryHistory.ts that mention security_assessment as an example, consider whether other parameters like security_assessment should also be tracked.

Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)

27-43: Review index strategy for potential redundancy.

Three indexes are created:

  1. Single column on parameter
  2. Single column on recorded_at DESC
  3. Composite on (parameter, recorded_at DESC)

The composite index can serve queries filtering by parameter alone, making the single-column parameter index potentially redundant. Consider whether all three indexes are needed based on actual query patterns, as each index adds overhead to write operations.

Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)

41-63: Satisfy the hooks dependency rule

fetchTimeseriesData is defined outside the effect but isn’t part of the dependency array, which will trip react-hooks/exhaustive-deps in our lint pipeline. To keep the build green, either wrap the fetcher in useCallback and include it in the dependency list or inline the async logic inside the effect.

-  useEffect(() => {
-    fetchTimeseriesData();
-  }, [timeframe, parameter]);
-
-  const fetchTimeseriesData = async () => {
+  useEffect(() => {
+    const fetchTimeseriesData = async () => {
       setLoading(true);
       setError(null);
       try {
         const response = await getModelInventoryTimeseries(parameter, timeframe);
         if (response?.data?.data) {
           setTimeseriesData(response.data.data.data);
         }
       } catch (err: any) {
         console.error("Error fetching timeseries data:", err);
         setError(err?.response?.data?.message || "Failed to load chart data");
       } finally {
         setLoading(false);
       }
     };
-  };
+
+    fetchTimeseriesData();
+  }, [parameter, timeframe]);
Servers/utils/history/riskHistory.utils.ts (1)

50-182: Consider extracting common counting pattern to reduce duplication.

The function contains four nearly identical blocks (severity, likelihood, mitigation_status, risk_level) that follow the same pattern: query counts, initialize all possible values to 0, then update with actual counts. This duplication increases maintenance burden.

Consider extracting a helper function:

async function getCountsForEnum(
  column: string,
  possibleValues: string[],
  tenant: string,
  transaction?: Transaction
): Promise<Record<string, number>> {
  const counts: Record<string, number> = {};
  
  const results = await sequelize.query(
    `SELECT ${column}, COUNT(*) as count
     FROM "${tenant}".risks
     WHERE is_deleted = false
     GROUP BY ${column}`,
    { type: QueryTypes.SELECT, transaction }
  ) as Array<{ [key: string]: any; count: string }>;
  
  possibleValues.forEach(value => counts[value] = 0);
  results.forEach(row => {
    if (row[column]) counts[row[column]] = parseInt(row.count, 10);
  });
  
  return counts;
}

Then simplify the main function to use this helper for severity, likelihood, mitigation_status, and risk_level.

Servers/utils/history/modelInventoryHistory.utils.ts (1)

1-481: Consider abstracting shared history logic to eliminate duplication.

This file is nearly identical to riskHistory.utils.ts (Servers/utils/history/riskHistory.utils.ts), with only table names and parameter-specific logic differing. This duplication means changes must be maintained in two places.

Consider creating a generic history utility that both files can use:

// historyBase.utils.ts
export function createHistoryUtils<TModel>(config: {
  tableName: string;
  historyTableName: string;
  model: typeof Model;
  parameterHandlers: Record<string, ParameterHandler>;
}) {
  return {
    recordHistorySnapshot: async (parameter, tenant, userId?, tx?) => {
      // Generic implementation using config.historyTableName
    },
    getCurrentParameterCounts: async (parameter, tenant, tx?) => {
      const handler = config.parameterHandlers[parameter];
      if (handler) return handler(tenant, tx);
      // Generic fallback with validation
    },
    // ... other functions
  };
}

Then instantiate for each domain:

// riskHistory.utils.ts
export const riskHistoryUtils = createHistoryUtils({
  tableName: 'risks',
  historyTableName: 'risk_history',
  model: RiskHistoryModel,
  parameterHandlers: { severity: handleSeverity, ... }
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff4cd2a and da52129.

📒 Files selected for processing (25)
  • Clients/src/application/repository/modelInventoryHistory.repository.ts (1 hunks)
  • Clients/src/application/repository/riskHistory.repository.ts (1 hunks)
  • Clients/src/presentation/components/AddNewRiskForm/index.tsx (4 hunks)
  • Clients/src/presentation/components/AnalyticsDrawer/index.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/pages/ModelInventory/index.tsx (6 hunks)
  • Clients/src/presentation/pages/RiskManagement/index.tsx (5 hunks)
  • Servers/controllers/modelInventoryHistory.ctrl.ts (1 hunks)
  • Servers/controllers/riskHistory.ctrl.ts (1 hunks)
  • Servers/database/db.ts (2 hunks)
  • Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1 hunks)
  • Servers/database/migrations/20251107000000-create-risk-history-table.js (1 hunks)
  • Servers/domain.layer/interfaces/i.modelInventoryHistory.ts (1 hunks)
  • Servers/domain.layer/interfaces/i.riskHistory.ts (1 hunks)
  • Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts (1 hunks)
  • Servers/domain.layer/models/riskHistory/riskHistory.model.ts (1 hunks)
  • Servers/index.ts (2 hunks)
  • Servers/routes/modelInventoryHistory.route.ts (1 hunks)
  • Servers/routes/riskHistory.route.ts (1 hunks)
  • Servers/scripts/createNewTenant.ts (1 hunks)
  • Servers/utils/history/modelInventoryHistory.utils.ts (1 hunks)
  • Servers/utils/history/riskHistory.utils.ts (1 hunks)
  • Servers/utils/modelInventory.utils.ts (4 hunks)
  • Servers/utils/risk.utils.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.

Applied to files:

  • Clients/src/presentation/components/AddNewRiskForm/index.tsx
🧬 Code graph analysis (17)
Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)
Servers/database/migrations/20251107000000-create-risk-history-table.js (7)
  • require (2-2)
  • transaction (7-7)
  • transaction (54-54)
  • organizations (9-12)
  • organizations (56-59)
  • tenantHash (15-15)
  • tenantHash (62-62)
Servers/controllers/riskHistory.ctrl.ts (3)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
  • getTimeseries (20-133)
  • getCurrentCounts (140-187)
  • createSnapshot (195-240)
Servers/utils/statusCode.utils.ts (1)
  • STATUS_CODE (1-98)
Servers/utils/history/riskHistory.utils.ts (4)
  • getTimeseriesForTimeframe (374-414)
  • getTimeseriesWithInterpolation (250-369)
  • getCurrentParameterCounts (50-182)
  • recordHistorySnapshot (8-45)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
  • getRiskTimeseries (13-29)
Servers/database/migrations/20251107000000-create-risk-history-table.js (1)
Servers/database/migrations/20251009120003-add-soft-delete-to-model-risks.js (1)
  • queryInterface (6-63)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getModelInventoryTimeseries (13-29)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
Servers/utils/statusCode.utils.ts (1)
  • STATUS_CODE (1-98)
Servers/utils/history/modelInventoryHistory.utils.ts (4)
  • getTimeseriesForTimeframe (317-357)
  • getTimeseriesWithInterpolation (193-312)
  • getCurrentParameterCounts (51-125)
  • recordHistorySnapshot (9-46)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getCurrentParameterCounts (37-47)
Servers/utils/risk.utils.ts (1)
Servers/utils/history/riskHistory.utils.ts (1)
  • recordSnapshotIfChanged (525-538)
Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts (2)
Servers/domain.layer/models/riskHistory/riskHistory.model.ts (1)
  • Table (5-61)
Servers/domain.layer/interfaces/i.modelInventoryHistory.ts (1)
  • IModelInventoryHistory (1-9)
Servers/domain.layer/models/riskHistory/riskHistory.model.ts (2)
Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts (1)
  • Table (5-61)
Servers/domain.layer/interfaces/i.riskHistory.ts (1)
  • IRiskHistory (1-9)
Servers/routes/riskHistory.route.ts (1)
Servers/controllers/riskHistory.ctrl.ts (3)
  • getTimeseries (20-147)
  • getCurrentCounts (154-215)
  • createSnapshot (223-282)
Clients/src/application/repository/modelInventoryHistory.repository.ts (2)
Clients/src/infrastructure/api/networkServices.ts (1)
  • apiServices (84-230)
Servers/utils/history/modelInventoryHistory.utils.ts (1)
  • getCurrentParameterCounts (51-125)
Clients/src/application/repository/riskHistory.repository.ts (1)
Clients/src/infrastructure/api/networkServices.ts (1)
  • apiServices (84-230)
Servers/utils/history/modelInventoryHistory.utils.ts (2)
Servers/utils/history/riskHistory.utils.ts (9)
  • recordHistorySnapshot (8-45)
  • getCurrentParameterCounts (50-182)
  • getTimeseriesData (187-217)
  • getTimeseriesWithInterpolation (250-369)
  • getTimeseriesForTimeframe (374-414)
  • getLatestSnapshot (419-443)
  • getSnapshotAtTime (448-477)
  • shouldRecordSnapshot (482-520)
  • recordSnapshotIfChanged (525-538)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getCurrentParameterCounts (37-47)
Clients/src/presentation/pages/ModelInventory/index.tsx (1)
Clients/src/presentation/pages/ModelInventory/style.ts (1)
  • addNewModelButtonStyle (26-29)
Servers/routes/modelInventoryHistory.route.ts (1)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
  • getTimeseries (20-133)
  • getCurrentCounts (140-187)
  • createSnapshot (195-240)
Servers/utils/modelInventory.utils.ts (2)
Servers/services/automations/automationProducer.ts (1)
  • enqueueAutomationAction (10-13)
Servers/utils/history/modelInventoryHistory.utils.ts (1)
  • recordSnapshotIfChanged (468-481)
Servers/utils/history/riskHistory.utils.ts (1)
Servers/utils/history/modelInventoryHistory.utils.ts (9)
  • recordHistorySnapshot (9-46)
  • getCurrentParameterCounts (51-125)
  • getTimeseriesData (130-160)
  • getTimeseriesWithInterpolation (193-312)
  • getTimeseriesForTimeframe (317-357)
  • getLatestSnapshot (362-386)
  • getSnapshotAtTime (391-420)
  • shouldRecordSnapshot (425-463)
  • recordSnapshotIfChanged (468-481)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/AddNewRiskForm/index.tsx

[error] 635-635: Type error: TS2345 - Argument of type 'RiskFormValues' is not assignable to parameter of type 'RiskFormValues & MitigationFormValues'. (in AddNewRiskForm)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (30)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (1)

564-575: LGTM: Risk level calculations correctly reference prefixed fields.

The conditional logic appropriately includes calculated risk levels when relevant fields change, and the prefixed field names align with the new approach.

Servers/domain.layer/interfaces/i.riskHistory.ts (1)

1-9: LGTM! Well-structured interface for risk history tracking.

The interface design is clean and appropriate for tracking historical snapshots. The use of Record<string, number> for JSONB counts is well-typed, and the inline comments provide helpful context for each field's purpose.

Servers/index.ts (1)

36-37: LGTM! Clean route integration.

The new history routes are properly imported and registered with logical placement near their related endpoints. The naming convention is consistent with the existing codebase.

Also applies to: 129-130

Clients/src/presentation/pages/RiskManagement/index.tsx (1)

530-546: LGTM! Well-configured analytics drawer integration.

The AnalyticsDrawer is properly configured with appropriate parameters for risk management analytics. The available parameters (severity, likelihood, mitigation_status, risk_level) align well with the risk domain, and the default parameter choice is sensible.

Servers/routes/modelInventoryHistory.route.ts (1)

1-19: LGTM! Clean route definitions with proper authentication.

The route structure is clean and properly secured with JWT authentication. The endpoints follow RESTful conventions.

Note: This file has an identical structure to riskHistory.route.ts. While this is acceptable, consider extracting a shared route factory function if you add more similar history routes in the future to reduce duplication.

Servers/routes/riskHistory.route.ts (1)

1-19: LGTM! Consistent route structure with proper authentication.

The route definitions are clean and follow the same pattern as the model inventory history routes, which promotes consistency across the codebase.

Servers/database/db.ts (1)

74-75: LGTM! Proper model registration for ORM.

The new history models are correctly imported and registered in the Sequelize models array, following the established pattern in the codebase.

Also applies to: 156-157

Servers/domain.layer/interfaces/i.modelInventoryHistory.ts (1)

1-9: LGTM!

The interface is well-designed with clear documentation for each field. The types are appropriate for tracking historical snapshots of model inventory parameters.

Servers/utils/risk.utils.ts (3)

560-573: LGTM!

The history recording is properly integrated after risk creation. Using Promise.all for concurrent snapshot recording is efficient, and the error handling ensures that history recording failures don't block risk creation.


918-931: LGTM!

History recording after deletion follows the same pattern as creation, with proper error handling that doesn't block the operation.


826-851: No issues found—parameter mapping is correct and intentional.

The mapping of risk_level (history parameter) to risk_level_autocalculated (model field) is intentional and properly implemented. The getCurrentParameterCounts function in riskHistory.utils.ts explicitly handles this at line 130–145, querying the risk_level_autocalculated column when the parameter is 'risk_level'. The reviewed code correctly uses 'risk_level_autocalculated' for the change comparison and passes the semantic parameter name 'risk_level' to recordSnapshotIfChanged, which is properly handled downstream.

Servers/utils/modelInventory.utils.ts (2)

424-433: LGTM!

The conditional history recording efficiently tracks status changes only when they occur, with proper null checking for oldModel.


498-505: LGTM!

History recording after deletion follows the established pattern with proper error handling.

Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)

16-25: Clarify the purpose of recorded_at vs created_at.

The table has both recorded_at and created_at timestamps. While this might be intentional (recorded_at = when the snapshot represents data from, created_at = when the row was inserted), consider documenting this distinction or evaluating if both are necessary.

Clients/src/presentation/components/AnalyticsDrawer/index.tsx (2)

8-22: LGTM!

The interface definitions are clear and well-structured, with appropriate optional fields and default values for backward compatibility.


137-149: LGTM with future extensibility note.

The conditional chart rendering is clean and works well for the current two chart types. As mentioned in the component documentation (line 18), this drawer is designed to be reusable for other entities like vendors. Consider using a switch statement or chart type registry if more chart types are added in the future.

Clients/src/application/repository/riskHistory.repository.ts (2)

56-70: LGTM!

The createRiskHistorySnapshot function correctly structures the POST request and returns response.data consistently.


37-47: Fix nested params object.

Line 40 incorrectly nests the parameter inside a params object. The apiServices.get method expects query parameters to be passed directly in the second argument, not nested within a params key.

Apply this diff:

 export async function getCurrentRiskParameterCounts(parameter: string): Promise<any> {
   try {
     const response = await apiServices.get("/api/riskHistory/current-counts", {
-      params: { parameter },
+      parameter,
     });
     return response.data;
   } catch (error: any) {
     console.error("Error fetching current risk parameter counts:", error);
     throw error;
   }
 }

Likely an incorrect or invalid review comment.

Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (4)

89-103: Update data access after fixing repository inconsistency.

Lines 94-96 access response?.data?.data, which assumes getRiskTimeseries returns the full response object. After fixing the getRiskTimeseries function in riskHistory.repository.ts to return response.data consistently, this code should be updated to access response?.data instead.

This depends on the fix suggested for Clients/src/application/repository/riskHistory.repository.ts lines 13-29.

   const fetchTimeseriesData = async () => {
     setLoading(true);
     setError(null);
     try {
       const response = await getRiskTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
+      if (response?.data) {
+        setTimeseriesData(response.data.data);
       }
     } catch (err: any) {
       console.error("Error fetching timeseries data:", err);
       setError(err?.response?.data?.message || "Failed to load chart data");
     } finally {
       setLoading(false);
     }
   };

110-140: LGTM!

The data preparation logic is well-structured, handles edge cases (empty data), and creates properly formatted series for the chart with appropriate styling.


144-200: LGTM!

The loading, error, and empty states are well-implemented with consistent styling and helpful user feedback.


226-299: LGTM!

The chart configuration is comprehensive and user-friendly, with proper scaling (15% headroom), integer tick steps for count data, and accessible date formatting.

Servers/controllers/modelInventoryHistory.ctrl.ts (2)

20-133: LGTM!

The getTimeseries endpoint has comprehensive input validation, supports multiple query modes (timeframe, custom range, default), and includes proper error handling with structured logging.


140-187: LGTM!

The getCurrentCounts endpoint is simple, focused, and properly validates input before delegating to the utility function.

Clients/src/application/repository/modelInventoryHistory.repository.ts (2)

56-70: LGTM!

The function correctly uses apiServices.post with proper body parameters and returns response.data consistently.


37-47: Correct the API call structure.

Line 40 incorrectly nests parameter inside a params object. According to apiServices.get (networkServices.ts:83-229), the second argument should be the params object directly, not nested.

Apply this diff:

 export async function getCurrentParameterCounts(parameter: string): Promise<any> {
   try {
     const response = await apiServices.get("/api/modelInventoryHistory/current-counts", {
-      params: { parameter },
+      parameter,
     });
     return response.data;
   } catch (error: any) {
     console.error("Error fetching current parameter counts:", error);
     throw error;
   }
 }

Likely an incorrect or invalid review comment.

Servers/utils/history/riskHistory.utils.ts (2)

8-45: LGTM!

The function correctly uses parameterized queries to prevent SQL injection and properly handles the snapshot creation with transaction support.


187-538: LGTM on the remaining functions!

All remaining functions (getTimeseriesData, getTimeseriesWithInterpolation, getTimeseriesForTimeframe, getLatestSnapshot, getSnapshotAtTime, shouldRecordSnapshot, recordSnapshotIfChanged) correctly use parameterized queries and implement sound interpolation and change-detection logic.

Servers/utils/history/modelInventoryHistory.utils.ts (2)

9-46: LGTM!

The function correctly uses parameterized queries and properly handles snapshot creation for model inventory history.


130-481: LGTM on timeseries and snapshot functions!

All timeseries, interpolation, and snapshot management functions correctly use parameterized queries and implement proper logic for historical data retrieval and change detection.

Comment on lines +13 to +29
export async function getModelInventoryTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/modelInventoryHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response;
} catch (error: any) {
console.error("Error fetching model inventory timeseries:", error);
throw error;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix inconsistent endpoint path and return value.

Two issues here:

  1. Line 21 uses /modelInventoryHistory/timeseries without the /api/ prefix, while other functions (lines 39, 61) use /api/modelInventoryHistory/.... This inconsistency will cause routing issues.
  2. Line 24 returns the full response object, while the other functions return response.data (lines 42, 65).

Apply this diff:

 export async function getModelInventoryTimeseries(
   parameter: string,
   timeframe?: string,
   startDate?: string,
   endDate?: string,
   intervalHours?: number
 ): Promise<any> {
   try {
-    const response = await apiServices.get("/modelInventoryHistory/timeseries", {
+    const response = await apiServices.get("/api/modelInventoryHistory/timeseries", {
       parameter, startDate, endDate, intervalHours, timeframe
     });
-    return response;
+    return response.data;
   } catch (error: any) {
     console.error("Error fetching model inventory timeseries:", error);
     throw error;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getModelInventoryTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/modelInventoryHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response;
} catch (error: any) {
console.error("Error fetching model inventory timeseries:", error);
throw error;
}
}
export async function getModelInventoryTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/api/modelInventoryHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response.data;
} catch (error: any) {
console.error("Error fetching model inventory timeseries:", error);
throw error;
}
}
🤖 Prompt for AI Agents
In Clients/src/application/repository/modelInventoryHistory.repository.ts around
lines 13 to 29, the timeseries call uses the wrong endpoint and returns the full
response: change the request path from "/modelInventoryHistory/timeseries" to
"/api/modelInventoryHistory/timeseries" to match other functions, and return
response.data (not response) so the function returns the same payload shape as
the other repository methods; preserve the same query object (parameter,
startDate, endDate, intervalHours, timeframe) when calling apiServices.get and
throw the error after logging as before.

Comment on lines +13 to +29
export async function getRiskTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/riskHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response;
} catch (error: any) {
console.error("Error fetching risk timeseries:", error);
throw error;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent return value - returns full response instead of response.data.

Lines 24 and 42 show inconsistency: getRiskTimeseries returns the full response object while getCurrentRiskParameterCounts (line 42) and createRiskHistorySnapshot (line 65) return response.data. This inconsistency could cause issues for consumers expecting the same structure.

Apply this diff to make the return value consistent:

 export async function getRiskTimeseries(
   parameter: string,
   timeframe?: string,
   startDate?: string,
   endDate?: string,
   intervalHours?: number
 ): Promise<any> {
   try {
     const response = await apiServices.get("/riskHistory/timeseries", {
       parameter, startDate, endDate, intervalHours, timeframe
     });
-    return response;
+    return response.data;
   } catch (error: any) {
     console.error("Error fetching risk timeseries:", error);
     throw error;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getRiskTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/riskHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response;
} catch (error: any) {
console.error("Error fetching risk timeseries:", error);
throw error;
}
}
export async function getRiskTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/riskHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
return response.data;
} catch (error: any) {
console.error("Error fetching risk timeseries:", error);
throw error;
}
}
🤖 Prompt for AI Agents
In Clients/src/application/repository/riskHistory.repository.ts around lines 13
to 29, getRiskTimeseries currently returns the full API response object while
other functions return response.data; update getRiskTimeseries to return
response.data instead of response so the return shape is consistent with
getCurrentRiskParameterCounts and createRiskHistorySnapshot, and keep the
existing try/catch and error rethrow behavior (adjust the Promise<any> type only
if you want stricter typing).

Comment on lines +65 to +67
const [selectedParameter, setSelectedParameter] = useState<string>(
defaultParameter || availableParameters[0]?.value || ""
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add validation for empty availableParameters.

If availableParameters is an empty array, selectedParameter will be an empty string, which could cause issues in the chart components or API calls. Consider adding validation or a fallback message when no parameters are available.

 const [selectedParameter, setSelectedParameter] = useState<string>(
   defaultParameter || availableParameters[0]?.value || ""
 );
+
+// Add early return if no parameters available
+if (availableParameters.length === 0) {
+  return (
+    <Drawer anchor="right" open={open} onClose={onClose} sx={{ "& .MuiDrawer-paper": { width: { xs: "100%", sm: "600px", md: "800px" }, padding: "24px" } }}>
+      <Typography>No parameters available for analytics</Typography>
+    </Drawer>
+  );
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Clients/src/presentation/components/AnalyticsDrawer/index.tsx around lines
65-67, the useState initializer assumes availableParameters has entries and
falls back to an empty string which can break charts or API calls; change
initialization and component behavior to explicitly handle an empty
availableParameters array by (1) initializing selectedParameter to null or
undefined when availableParameters is empty or when no defaultParameter exists,
(2) adding a conditional render or disabled state in the UI to show a clear "No
parameters available" message and prevent chart/API usage, and (3) adding guards
where selectedParameter is used (charts, API calls) to skip calls or render
placeholders when it is null/undefined. Ensure any downstream code checks for
the empty state to avoid passing an empty string to chart components or
requests.

Comment on lines +195 to +240
export async function createSnapshot(req: Request, res: Response) {
logStructured(
"processing",
"starting createSnapshot",
"createSnapshot",
"modelInventoryHistory.ctrl.ts"
);
logger.debug("📸 Creating manual history snapshot");

try {
const { parameter } = req.body;

if (!parameter) {
logStructured(
"error",
"parameter is required",
"createSnapshot",
"modelInventoryHistory.ctrl.ts"
);
return res.status(400).json(STATUS_CODE[400]("Parameter is required"));
}

const userId = req.userId!;
const snapshot = await recordHistorySnapshot(parameter, req.tenantId!, userId);

logStructured(
"successful",
`snapshot created for parameter ${parameter}`,
"createSnapshot",
"modelInventoryHistory.ctrl.ts"
);

return res.status(201).json(
STATUS_CODE[201](snapshot.toJSON())
);
} catch (error) {
logStructured(
"error",
"error creating snapshot",
"createSnapshot",
"modelInventoryHistory.ctrl.ts",
);
logger.error("❌ Error creating snapshot:", error);
return res.status(500).json(STATUS_CODE[500]((error as Error).message));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix misleading comment about change_description.

The comment at lines 193-194 states "The snapshot counts will be automatically stored in change_description as JSONB", but this is incorrect. Based on the interface IModelInventoryHistory and the utility function recordHistorySnapshot, counts are stored in snapshot_data, not change_description. The change_description field is an optional text field for user-provided descriptions.

Apply this diff to fix the comment:

 /**
  * Manually trigger a history snapshot
  * Body params:
  * - parameter: string (required) - The parameter to snapshot
- * Note: The snapshot counts will be automatically stored in change_description as JSONB
+ * - description: string (optional) - Optional description of what changed
  */

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Servers/controllers/modelInventoryHistory.ctrl.ts around lines 195 to 240,
update the misleading comment that says "The snapshot counts will be
automatically stored in change_description as JSONB" to state that counts are
stored in snapshot_data (not change_description) and that change_description is
an optional text field for user-provided descriptions; change the comment text
accordingly so it matches the IModelInventoryHistory interface and the
recordHistorySnapshot implementation.

Comment on lines +83 to +117
const intervalHours = req.query.intervalHours
? parseInt(req.query.intervalHours as string, 10)
: 24;

if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
logStructured(
"error",
"invalid date format",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Invalid date format. Use ISO date format.")
);
}

if (startDate >= endDate) {
logStructured(
"error",
"start date must be before end date",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Start date must be before end date")
);
}

timeseriesData = await getTimeseriesWithInterpolation(
parameter,
startDate,
endDate,
req.tenantId!,
intervalHours
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validate intervalHours before using it

We accept any intervalHours and pass it straight to getTimeseriesWithInterpolation. A zero, negative, or non-numeric value makes the while-loop in that util never advance, tying up the worker thread and effectively DoSing the endpoint. Please validate that the parsed value is a finite, positive integer before proceeding.

-      const intervalHours = req.query.intervalHours
-        ? parseInt(req.query.intervalHours as string, 10)
-        : 24;
+      const rawInterval = req.query.intervalHours
+        ? Number.parseInt(req.query.intervalHours as string, 10)
+        : 24;
+
+      if (!Number.isFinite(rawInterval) || rawInterval <= 0) {
+        logStructured(
+          "error",
+          "invalid intervalHours",
+          "getTimeseries",
+          "riskHistory.ctrl.ts"
+        );
+        return res
+          .status(400)
+          .json(STATUS_CODE[400]("intervalHours must be a positive integer"));
+      }
+
+      const intervalHours = rawInterval;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const intervalHours = req.query.intervalHours
? parseInt(req.query.intervalHours as string, 10)
: 24;
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
logStructured(
"error",
"invalid date format",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Invalid date format. Use ISO date format.")
);
}
if (startDate >= endDate) {
logStructured(
"error",
"start date must be before end date",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Start date must be before end date")
);
}
timeseriesData = await getTimeseriesWithInterpolation(
parameter,
startDate,
endDate,
req.tenantId!,
intervalHours
);
const rawInterval = req.query.intervalHours
? Number.parseInt(req.query.intervalHours as string, 10)
: 24;
if (!Number.isFinite(rawInterval) || rawInterval <= 0) {
logStructured(
"error",
"invalid intervalHours",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res
.status(400)
.json(STATUS_CODE[400]("intervalHours must be a positive integer"));
}
const intervalHours = rawInterval;
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
logStructured(
"error",
"invalid date format",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Invalid date format. Use ISO date format.")
);
}
if (startDate >= endDate) {
logStructured(
"error",
"start date must be before end date",
"getTimeseries",
"riskHistory.ctrl.ts"
);
return res.status(400).json(
STATUS_CODE[400]("Start date must be before end date")
);
}
timeseriesData = await getTimeseriesWithInterpolation(
parameter,
startDate,
endDate,
req.tenantId!,
intervalHours
);
🤖 Prompt for AI Agents
In Servers/controllers/riskHistory.ctrl.ts around lines 83 to 117, the parsed
intervalHours is not validated and a zero, negative or non-numeric value can
hang the worker; after parsing intervalHours (or defaulting to 24) validate that
it is a finite, positive integer (> 0) using Number.isFinite/Number.isInteger
(or equivalent) and reject invalid values by returning res.status(400) with a
clear message (e.g., "intervalHours must be a positive integer"); only pass the
validated integer to getTimeseriesWithInterpolation.

Comment on lines +17 to +24
CREATE TABLE "${tenantHash}".risk_history (
id SERIAL PRIMARY KEY,
parameter VARCHAR(255) NOT NULL,
snapshot_data JSONB NOT NULL DEFAULT '{}',
recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add the missing change_description column

RiskHistoryModel (and IRiskHistory) define a change_description attribute, but the table created here does not. As soon as the code attempts to persist a snapshot with a description, Postgres will reject the insert (column "change_description" does not exist). Please align the DDL with the model by adding the nullable text column before merging.

         await queryInterface.sequelize.query(`
           CREATE TABLE "${tenantHash}".risk_history (
             id SERIAL PRIMARY KEY,
             parameter VARCHAR(255) NOT NULL,
             snapshot_data JSONB NOT NULL DEFAULT '{}',
             recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
             triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
+            change_description TEXT,
             created_at TIMESTAMP DEFAULT NOW()
           );
         `, { transaction });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE "${tenantHash}".risk_history (
id SERIAL PRIMARY KEY,
parameter VARCHAR(255) NOT NULL,
snapshot_data JSONB NOT NULL DEFAULT '{}',
recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE "${tenantHash}".risk_history (
id SERIAL PRIMARY KEY,
parameter VARCHAR(255) NOT NULL,
snapshot_data JSONB NOT NULL DEFAULT '{}',
recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
change_description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
🤖 Prompt for AI Agents
In Servers/database/migrations/20251107000000-create-risk-history-table.js
around lines 17 to 24 the DDL creates risk_history but omits the
change_description column defined on RiskHistoryModel/IRiskHistory; add a
nullable text column named change_description (e.g., change_description TEXT) to
the table definition (positioning it among the other columns, before created_at
if you prefer) so inserts with a description will succeed.

Comment on lines +102 to +118
} else {
// Generic handling for other parameters
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".model_inventories
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;

paramCounts.forEach((row) => {
const key = row[parameter] !== null ? String(row[parameter]) : 'null';
counts[key] = parseInt(row.count, 10);
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: SQL injection vulnerability in generic parameter handling.

Lines 105, 107: The parameter variable is directly interpolated into the SQL query without validation or parameterization, allowing potential SQL injection if an attacker controls the parameter value.

Apply this diff to add parameter validation:

     } else {
       // Generic handling for other parameters
+      // Whitelist allowed parameters to prevent SQL injection
+      const allowedParameters = ['created_at', 'updated_at', 'version', 'owner']; // Add safe column names
+      if (!allowedParameters.includes(parameter)) {
+        throw new Error(`Invalid parameter: ${parameter}`);
+      }
+      
       const paramCounts = await sequelize.query(
         `SELECT ${parameter}, COUNT(*) as count
          FROM "${tenant}".model_inventories
          GROUP BY ${parameter}`,
         {
           type: QueryTypes.SELECT,
           transaction,
         }
       ) as Array<{ [key: string]: any; count: string }>;

Alternatively, use Sequelize's query builder for safer dynamic column queries.

🤖 Prompt for AI Agents
In Servers/utils/history/modelInventoryHistory.utils.ts around lines 102 to 118,
the code injects the variable `parameter` directly into a raw SQL string
creating an SQL injection risk; replace this with a safe approach by validating
`parameter` against a predefined whitelist of allowed column names (and only
proceed if it matches exactly), and ensure `tenant` is also validated/normalized
before using it in the table name; then build the query using the validated,
constant column name (or use Sequelize column quoting via QueryInterface or
model attribute mapping) and continue to use parameterized options for
values/transactions so no user-controlled input is concatenated into SQL.

Comment on lines +158 to +175
} else {
// Generic handling for other parameters
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".risks
WHERE is_deleted = false
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;

paramCounts.forEach((row) => {
const key = row[parameter] !== null ? String(row[parameter]) : 'null';
counts[key] = parseInt(row.count, 10);
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: SQL injection vulnerability in generic parameter handling.

Lines 161, 164: The parameter variable is directly interpolated into the SQL query without validation or parameterization. If an attacker controls the parameter value, they could inject arbitrary SQL.

Apply this diff to use parameterized queries or implement a whitelist:

     } else {
       // Generic handling for other parameters
+      // Whitelist allowed parameters to prevent SQL injection
+      const allowedParameters = ['created_at', 'updated_at']; // Add other safe column names
+      if (!allowedParameters.includes(parameter)) {
+        throw new Error(`Invalid parameter: ${parameter}`);
+      }
+      
       const paramCounts = await sequelize.query(
         `SELECT ${parameter}, COUNT(*) as count
          FROM "${tenant}".risks
          WHERE is_deleted = false
          GROUP BY ${parameter}`,
         {
           type: QueryTypes.SELECT,
           transaction,
         }
       ) as Array<{ [key: string]: any; count: string }>;

Alternatively, use Sequelize's query builder instead of raw SQL for safer dynamic queries.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
// Generic handling for other parameters
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".risks
WHERE is_deleted = false
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;
paramCounts.forEach((row) => {
const key = row[parameter] !== null ? String(row[parameter]) : 'null';
counts[key] = parseInt(row.count, 10);
});
}
} else {
// Generic handling for other parameters
// Whitelist allowed parameters to prevent SQL injection
const allowedParameters = ['created_at', 'updated_at']; // Add other safe column names
if (!allowedParameters.includes(parameter)) {
throw new Error(`Invalid parameter: ${parameter}`);
}
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".risks
WHERE is_deleted = false
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;
paramCounts.forEach((row) => {
const key = row[parameter] !== null ? String(row[parameter]) : 'null';
counts[key] = parseInt(row.count, 10);
});
}

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (9)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)

431-454: Avoid any in generic types for better type safety.

The generic constraint Record<string, any> and return type Record<string, any> reduce type safety. Consider using more specific types or at least unknown instead of any. Based on learnings.

Additionally, the array comparison at lines 441-443 only works correctly for arrays of primitives or simple objects. For nested objects, consider using a deep equality library or a more robust comparison method.

Apply this diff to improve type safety:

-  const getChangedFields = useCallback(<T extends Record<string, any>>(original: Partial<T>, current: Partial<T>) => {
-    const changedFields: Record<string, any> = {};
+  const getChangedFields = useCallback(<T extends Record<string, unknown>>(original: Partial<T>, current: Partial<T>): Partial<T> => {
+    const changedFields: Partial<T> = {};

For the array comparison, consider adding a comment clarifying the limitation:

-      // For arrays, do deep comparison (use copies to avoid mutating originals)
+      // For arrays of primitives, do sorted comparison (use copies to avoid mutating originals)
+      // Note: This comparison may not work correctly for arrays of complex nested objects
       if (Array.isArray(originalValue) && Array.isArray(currentValue)) {

524-553: Consider type-safe field mapping.

The manual field mapping is extensive and could be error-prone during maintenance. Consider using TypeScript's type system to ensure the mapping keys match the actual field names, or extract this mapping to a shared constant file for reusability.

For example, you could create a type-safe mapping helper:

// Define types for risk and mitigation field names
type RiskFieldKey = keyof RiskFormValues;
type MitigationFieldKey = keyof MitigationFormValues;

// Create a mapping that ensures compile-time checking
const createFieldMapping = () => {
  const riskMapping: Record<`risk_${RiskFieldKey}`, string> = {
    risk_riskName: 'risk_name',
    risk_actionOwner: 'risk_owner',
    // ... other risk fields
  };
  
  const mitigationMapping: Record<`mitigation_${MitigationFieldKey}`, string> = {
    mitigation_mitigationStatus: 'mitigation_status',
    // ... other mitigation fields
  };
  
  return { ...riskMapping, ...mitigationMapping };
};
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (3)

39-39: Define a proper type interface for timeseries data.

The any[] type bypasses TypeScript's type checking and could hide bugs. Define an interface that matches the expected shape of the API response.

Consider creating an interface like:

interface TimeseriesDataPoint {
  timestamp: string;
  data: Record<ModelInventoryStatus, number>;
}

Then update the state declaration:

-  const [timeseriesData, setTimeseriesData] = useState<any[]>([]);
+  const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);

66-92: Memoize expensive chart data preparation.

prepareChartData recalculates on every render, even when timeseriesData hasn't changed. This is inefficient, especially with large datasets.

Apply this diff:

+  const { timestamps, series, maxValue } = React.useMemo(() => {
-  // Prepare data for the chart
-  const prepareChartData = () => {
     if (!timeseriesData || timeseriesData.length === 0) {
       return { timestamps: [], series: [], maxValue: 0 };
     }
 
     const timestamps = timeseriesData.map((point) => new Date(point.timestamp));
 
     // Get all unique status values
     const statusValues = Object.values(ModelInventoryStatus);
     const series = statusValues.map((status) => ({
       label: status,
       data: timeseriesData.map((point) => point.data[status] || 0),
       color: STATUS_COLORS[status] || "#9CA3AF",
       curve: "monotoneX" as const,
       showMark: false,
     }));
 
     // Calculate max value across all series for proper y-axis scaling
     const maxValue = Math.max(
       ...series.flatMap((s) => s.data),
       0
     );
 
     return { timestamps, series, maxValue };
-  };
-
-  const { timestamps, series, maxValue } = prepareChartData();
+  }, [timeseriesData]);

94-110: Extract magic number to a named constant.

The 120 added to height appears in multiple places (lines 101, 119, 139) but its purpose isn't clear. This represents the additional space needed for title, controls, and padding.

Consider extracting to a constant at the top of the file:

+const CHART_CONTAINER_EXTRA_HEIGHT = 120; // Space for title, controls, and padding
+
 const ModelInventoryHistoryChart: React.FC<ModelInventoryHistoryChartProps> = ({

Then use it consistently:

-          height: height + 120,
+          height: height + CHART_CONTAINER_EXTRA_HEIGHT,
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (4)

8-12: Consider exporting the interface for consumers.

The RiskHistoryChartProps interface is not currently exported. If other components need to reference this type (e.g., when wrapping or extending this component), they won't have access to it.

Apply this diff to export the interface:

-interface RiskHistoryChartProps {
+export interface RiskHistoryChartProps {
   parameter?: string;
   title?: string;
   height?: number;
 }

83-83: Replace any[] with a typed interface.

Using any[] for timeseriesData bypasses TypeScript's type checking and can lead to runtime errors if the data structure changes.

Define an interface for the timeseries data:

interface TimeseriesPoint {
  timestamp: string;
  data: Record<string, number>;
}

Then update the state declaration:

-  const [timeseriesData, setTimeseriesData] = useState<any[]>([]);
+  const [timeseriesData, setTimeseriesData] = useState<TimeseriesPoint[]>([]);

85-103: Consider wrapping fetchTimeseriesData in useCallback.

The function fetchTimeseriesData is called in the useEffect but isn't listed in the dependency array, which may trigger ESLint warnings. While the current implementation is functionally correct (the function captures parameter and timeframe from its closure), wrapping it in useCallback would make the dependency flow explicit and satisfy the linter.

-  const fetchTimeseriesData = async () => {
+  const fetchTimeseriesData = useCallback(async () => {
     setLoading(true);
     setError(null);
     try {
       const response = await getRiskTimeseries(parameter, timeframe);
       if (response?.data?.data) {
         setTimeseriesData(response.data.data.data);
       }
     } catch (err: any) {
       console.error("Error fetching timeseries data:", err);
       setError(err?.response?.data?.message || "Failed to load chart data");
     } finally {
       setLoading(false);
     }
-  };
+  }, [parameter, timeframe]);
 
   useEffect(() => {
     fetchTimeseriesData();
-  }, [timeframe, parameter]);
+  }, [fetchTimeseriesData]);

247-247: Extract magic numbers as named constants.

The values 1.15 (y-axis padding multiplier) and 10 (fallback maximum) are magic numbers that reduce code readability. Extracting them as named constants would make their purpose clearer.

Add constants near the top of the file (after the color maps):

const Y_AXIS_PADDING_MULTIPLIER = 1.15; // Add 15% padding above max value
const Y_AXIS_DEFAULT_MAX = 10; // Default max when no data

Then update line 247:

-                max: maxValue > 0 ? Math.ceil(maxValue * 1.15) : 10,
+                max: maxValue > 0 ? Math.ceil(maxValue * Y_AXIS_PADDING_MULTIPLIER) : Y_AXIS_DEFAULT_MAX,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da52129 and bcd279b.

📒 Files selected for processing (3)
  • Clients/src/presentation/components/AddNewRiskForm/index.tsx (5 hunks)
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.

Applied to files:

  • Clients/src/presentation/components/AddNewRiskForm/index.tsx
🧬 Code graph analysis (2)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
  • getRiskTimeseries (13-29)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getModelInventoryTimeseries (13-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)

563-574: LGTM!

The logic correctly determines when to include calculated risk levels based on the new prefixed field names. The separation between risk-related and mitigation-related field changes is clear and appropriate.


633-664: LGTM!

The update logic correctly separates risk and mitigation changed fields, applies appropriate prefixes, and updates the deletion flag checks to use the new prefixed field names. The implementation is consistent with the new analytics infrastructure described in the PR summary.

Comment on lines +41 to +59
useEffect(() => {
fetchTimeseriesData();
}, [timeframe, parameter]);

const fetchTimeseriesData = async () => {
setLoading(true);
setError(null);
try {
const response = await getModelInventoryTimeseries(parameter, timeframe);
if (response?.data?.data) {
setTimeseriesData(response.data.data.data);
}
} catch (err: any) {
console.error("Error fetching timeseries data:", err);
setError(err?.response?.data?.message || "Failed to load chart data");
} finally {
setLoading(false);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add cleanup to prevent state updates on unmounted component.

The useEffect doesn't return a cleanup function. If the component unmounts while the API call is in flight, calling setState will trigger a React warning and potential memory leak.

Apply this diff:

   useEffect(() => {
-    fetchTimeseriesData();
+    let isMounted = true;
+    
+    const fetchData = async () => {
+      setLoading(true);
+      setError(null);
+      try {
+        const response = await getModelInventoryTimeseries(parameter, timeframe);
+        if (isMounted && response?.data?.data) {
+          setTimeseriesData(response.data.data.data);
+        }
+      } catch (err: any) {
+        console.error("Error fetching timeseries data:", err);
+        if (isMounted) {
+          setError(err?.response?.data?.message || "Failed to load chart data");
+        }
+      } finally {
+        if (isMounted) {
+          setLoading(false);
+        }
+      }
+    };
+    
+    fetchData();
+    
+    return () => {
+      isMounted = false;
+    };
   }, [timeframe, parameter]);
-
-  const fetchTimeseriesData = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
-      }
-    } catch (err: any) {
-      console.error("Error fetching timeseries data:", err);
-      setError(err?.response?.data?.message || "Failed to load chart data");
-    } finally {
-      setLoading(false);
-    }
-  };
🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
around lines 41 to 59, the effect starts an async fetch but doesn't clean up, so
setState may run after unmount; fix by guarding state updates with an isMounted
flag (or AbortController if the HTTP client supports cancellation): create let
isMounted = true before calling fetchTimeseriesData, check isMounted before
calling setLoading, setTimeseriesData, and setError inside the async flow (or
attach signal to the request and skip updates if aborted), and return a cleanup
function from useEffect that sets isMounted = false (or aborts the controller)
to prevent state updates on an unmounted component.

Comment on lines +50 to +52
if (response?.data?.data) {
setTimeseriesData(response.data.data.data);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Simplify deeply nested data access.

The path response.data.data.data is confusing and fragile. If the API structure changes slightly, this will break silently.

Consider either:

  1. Normalizing the repository response to remove redundant nesting
  2. Adding clearer intermediate variables:
       const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
+      const apiData = response?.data?.data;
+      if (apiData?.data) {
+        setTimeseriesData(apiData.data);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response?.data?.data) {
setTimeseriesData(response.data.data.data);
}
const apiData = response?.data?.data;
if (apiData?.data) {
setTimeseriesData(apiData.data);
}
🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
around lines 50-52, the code reads response.data.data.data which is overly
nested and fragile; replace this with a single, clear source by either updating
the repository/API layer to return a normalized payload (e.g., remove the extra
"data" wrapper) or introduce a descriptive intermediate variable (e.g., const
payload = response?.data?.data ?? response?.data ?? {}; then use payload.data or
payload.items) and use optional chaining with a sensible default when calling
setTimeseriesData so the component won’t break on small shape changes.

margin={{ top: 20, right: 30, bottom: 80, left: 70 }}
slotProps={{
legend: {
direction: "row" as any,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Replace as any type assertion with proper typing.

The as any assertion bypasses type checking and may hide incompatibilities with the MUI X-Charts API. The direction property should have a proper type from the library.

Verify the correct type for the direction property in the current version of @mui/x-charts (v8.4.0):


🌐 Web query:

What are the valid types for the legend direction property in @mui/x-charts version 8.4.0?

💡 Result:

The legend direction accepts: 'horizontal' or 'vertical'. [1][2]


Fix invalid direction value in legend configuration.

The direction property only accepts 'horizontal' or 'vertical' in @mui/x-charts v8.4.0, but the code uses "row". The valid types for the legend direction property are 'horizontal' or 'vertical'. Replace direction: "row" as any with the correct value ('horizontal' or 'vertical' depending on intended layout).

🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/RiskHistoryChart.tsx around line
265, the legend configuration uses an invalid direction value ("row" as any);
replace it with a valid @mui/x-charts v8.4.0 value — either 'horizontal' or
'vertical' depending on the intended layout (most likely 'horizontal' for a row
layout) and remove the cast.

Comment on lines +267 to +276
// padding: { top: 30 },
// itemMarkWidth: 14,
// itemMarkHeight: 14,
// markGap: 8,
// itemGap: 24,
// labelStyle: {
// fontSize: 13,
// fontWeight: 500,
// fill: "#1c2130",
// },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove commented-out code.

Dead code should be removed before merging. If these legend customization options are needed in the future, they can be retrieved from version control history.

Apply this diff to remove the commented lines:

               legend: {
                 direction: "row" as any,
                 position: { vertical: "bottom", horizontal: "center" },
-                // padding: { top: 30 },
-                // itemMarkWidth: 14,
-                // itemMarkHeight: 14,
-                // markGap: 8,
-                // itemGap: 24,
-                // labelStyle: {
-                //   fontSize: 13,
-                //   fontWeight: 500,
-                //   fill: "#1c2130",
-                // },
               },
🤖 Prompt for AI Agents
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx around lines 267
to 276: remove the block of commented-out legend customization lines (padding,
itemMarkWidth, itemMarkHeight, markGap, itemGap, labelStyle and their values) so
the file contains no dead commented code; simply delete those lines and run a
quick lint/format to ensure spacing remains consistent.

@MuhammadKhalilzadeh
Copy link
Copy Markdown
Collaborator

One of the checks has failed @HarshP4585

@MuhammadKhalilzadeh
Copy link
Copy Markdown
Collaborator

@rachanabasnet and @Br0wnHammer Would you please provide a review here?

Harsh and others added 2 commits November 12, 2025 13:11
- Update drawer header font size to 15px for consistency
- Restructure parameter selection layout with text on top and buttons below
- Align parameter selection buttons to the right
- Remove chart titles and show only timeframe toggles
- Adjust chart margins and legend positioning (35px padding)
- Reduce chart height from 400px to 300px
- Add EmptyState component with message "There is no historical data here"
- Apply consistent styling across Model Inventory and Risk Management analytics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gorkem-bwl gorkem-bwl mentioned this pull request Nov 13, 2025
29 tasks
Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (6)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (3)

50-52: Simplify deeply nested data access.

The path response.data.data.data is confusing and fragile. If the API structure changes slightly, this will break silently.

Based on learnings.

Consider adding clearer intermediate variables:

       const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
+      const apiPayload = response?.data?.data;
+      if (apiPayload?.data) {
+        setTimeseriesData(apiPayload.data);
       }

42-60: Add cleanup to prevent state updates on unmounted component.

The useEffect doesn't return a cleanup function. If the component unmounts while the API call is in flight, calling setState will trigger a React warning and potential memory leak.

Based on learnings.

Apply this diff:

   useEffect(() => {
-    fetchTimeseriesData();
+    let isMounted = true;
+    
+    const fetchData = async () => {
+      setLoading(true);
+      setError(null);
+      try {
+        const response = await getModelInventoryTimeseries(parameter, timeframe);
+        if (isMounted && response?.data?.data) {
+          setTimeseriesData(response.data.data.data);
+        }
+      } catch (err: any) {
+        console.error("Error fetching timeseries data:", err);
+        if (isMounted) {
+          setError(err?.response?.data?.message || "Failed to load chart data");
+        }
+      } finally {
+        if (isMounted) {
+          setLoading(false);
+        }
+      }
+    };
+    
+    fetchData();
+    
+    return () => {
+      isMounted = false;
+    };
   }, [timeframe, parameter]);
-
-  const fetchTimeseriesData = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
-      }
-    } catch (err: any) {
-      console.error("Error fetching timeseries data:", err);
-      setError(err?.response?.data?.message || "Failed to load chart data");
-    } finally {
-      setLoading(false);
-    }
-  };

204-204: Replace type assertion with proper typing.

The as any assertion bypasses TypeScript's type checking. According to @mui/x-charts v8.4.0 documentation, the direction property accepts 'horizontal' or 'vertical'.

Based on learnings.

Apply this diff:

                 legend: {
                   hidden: false,
-                  direction: "row" as any,
+                  direction: "horizontal",
                   position: { vertical: "bottom", horizontal: "center" },
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (3)

94-96: Simplify deeply nested data access.

The path response.data.data.data is confusing and fragile. If the API structure changes slightly, this will break silently.

Based on learnings.

Consider adding clearer intermediate variables:

       const response = await getRiskTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
+      const apiPayload = response?.data?.data;
+      if (apiPayload?.data) {
+        setTimeseriesData(apiPayload.data);
       }

86-104: Add cleanup to prevent state updates on unmounted component.

The useEffect doesn't return a cleanup function. If the component unmounts while the API call is in flight, calling setState will trigger a React warning and potential memory leak.

Based on learnings.

Apply this diff:

   useEffect(() => {
-    fetchTimeseriesData();
+    let isMounted = true;
+    
+    const fetchData = async () => {
+      setLoading(true);
+      setError(null);
+      try {
+        const response = await getRiskTimeseries(parameter, timeframe);
+        if (isMounted && response?.data?.data) {
+          setTimeseriesData(response.data.data.data);
+        }
+      } catch (err: any) {
+        console.error("Error fetching timeseries data:", err);
+        if (isMounted) {
+          setError(err?.response?.data?.message || "Failed to load chart data");
+        }
+      } finally {
+        if (isMounted) {
+          setLoading(false);
+        }
+      }
+    };
+    
+    fetchData();
+    
+    return () => {
+      isMounted = false;
+    };
   }, [timeframe, parameter]);
-
-  const fetchTimeseriesData = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const response = await getRiskTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
-      }
-    } catch (err: any) {
-      console.error("Error fetching timeseries data:", err);
-      setError(err?.response?.data?.message || "Failed to load chart data");
-    } finally {
-      setLoading(false);
-    }
-  };

254-254: Replace invalid direction value with proper type.

The direction property only accepts 'horizontal' or 'vertical' in @mui/x-charts v8.4.0, but the code uses "row" as any. The type assertion bypasses the error but doesn't fix the incorrect value.

Based on learnings.

Apply this diff:

                 legend: {
                   hidden: false,
-                  direction: "row" as any,
+                  direction: "horizontal",
                   position: { vertical: "bottom", horizontal: "center" },
🧹 Nitpick comments (2)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)

40-40: Consider adding a more specific type for timeseries data.

Using any[] reduces type safety and makes the code harder to maintain. Consider defining an interface for the timeseries data points.

Example:

interface TimeseriesDataPoint {
  timestamp: string;
  data: Record<string, number>;
}

const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)

84-84: Consider adding a more specific type for timeseries data.

Using any[] reduces type safety and makes the code harder to maintain. Consider defining an interface for the timeseries data points.

Example:

interface TimeseriesDataPoint {
  timestamp: string;
  data: Record<string, number>;
}

const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba30fac and 9a00d76.

📒 Files selected for processing (3)
  • Clients/src/presentation/components/AnalyticsDrawer/index.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Clients/src/presentation/components/AnalyticsDrawer/index.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.

Applied to files:

  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-01-23T05:13:38.328Z
Learnt from: samuel-coutinho
Repo: bluewave-labs/verifywise PR: 524
File: Clients/src/presentation/pages/ProjectView/ProjectSettings/index.tsx:63-63
Timestamp: 2025-01-23T05:13:38.328Z
Learning: Always handle loading and error states in components that fetch data to prevent crashes and provide better UX. Use loading indicators during data fetching and display appropriate error messages when API calls fail.

Applied to files:

  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-08-07T21:26:04.614Z
Learnt from: Br0wnHammer
Repo: bluewave-labs/verifywise PR: 1872
File: Clients/src/application/hooks/useDashboard.ts:12-13
Timestamp: 2025-08-07T21:26:04.614Z
Learning: In the verifywise project, the `getAllEntities` function returns a response object where the actual data is nested within a `data` property, so the correct access pattern is `response.data` rather than accessing the response directly.

Applied to files:

  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
🧬 Code graph analysis (2)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
  • getRiskTimeseries (13-29)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getModelInventoryTimeseries (13-29)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx

[error] 34-34: TS6133: 'title' is declared but its value is never read. (build step: npm run build)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)

gorkem-bwl and others added 2 commits November 12, 2025 23:49
- Remove unused 'title' parameter from both chart components
- Remove 'hidden' property from legend config (not a valid property)
- Legend is shown by default without the hidden property

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove padding property from legend config in both chart components
- Property is not supported by MUI Charts legend type
- Legend positioning controlled through bottom margin instead

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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: 1

♻️ Duplicate comments (4)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (4)

50-52: Simplify deeply nested data access.

The path response.data.data.data is confusing and fragile. If the API structure changes slightly, this will break silently.

Consider adding clearer intermediate variables:

       const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
+      const apiData = response?.data?.data;
+      if (apiData?.data) {
+        setTimeseriesData(apiData.data);
       }

202-202: Remove type assertion and use correct type.

The as any type assertion bypasses TypeScript's type checking.

Check the MUI X-Charts documentation for the correct direction type. If "row" is valid, the type definition should accept it without assertion. Otherwise, use the proper enum or literal type:

             slotProps={{
               legend: {
-                direction: "row" as any,
+                direction: "row",
                 position: { vertical: "bottom", horizontal: "center" },

10-14: Remove or use the unused 'title' prop.

The title parameter is declared but never used. Either remove it from the interface if not needed, or render it in the UI (e.g., as a chart heading).

Apply this diff to render the title:

     <Stack spacing={3}>
+      {title && (
+        <Typography sx={{ fontSize: 16, fontWeight: 600, color: "#101828" }}>
+          {title}
+        </Typography>
+      )}
       <Stack direction="row" justifyContent="flex-end" alignItems="center">

Or remove it from the props interface:

 interface ModelInventoryHistoryChartProps {
   parameter?: string;
-  title?: string;
   height?: number;
 }

41-59: Add cleanup to prevent state updates on unmounted component.

The useEffect doesn't return a cleanup function. If the component unmounts while the API call is in flight, calling setState will trigger a React warning and potential memory leak.

Apply this diff:

   useEffect(() => {
-    fetchTimeseriesData();
+    let isMounted = true;
+    
+    const fetchData = async () => {
+      setLoading(true);
+      setError(null);
+      try {
+        const response = await getModelInventoryTimeseries(parameter, timeframe);
+        if (isMounted && response?.data?.data) {
+          setTimeseriesData(response.data.data.data);
+        }
+      } catch (err: any) {
+        console.error("Error fetching timeseries data:", err);
+        if (isMounted) {
+          setError(err?.response?.data?.message || "Failed to load chart data");
+        }
+      } finally {
+        if (isMounted) {
+          setLoading(false);
+        }
+      }
+    };
+    
+    fetchData();
+    
+    return () => {
+      isMounted = false;
+    };
   }, [timeframe, parameter]);
-
-  const fetchTimeseriesData = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const response = await getModelInventoryTimeseries(parameter, timeframe);
-      if (response?.data?.data) {
-        setTimeseriesData(response.data.data.data);
-      }
-    } catch (err: any) {
-      console.error("Error fetching timeseries data:", err);
-      setError(err?.response?.data?.message || "Failed to load chart data");
-    } finally {
-      setLoading(false);
-    }
-  };

Based on learnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a00d76 and 5f67ea6.

📒 Files selected for processing (2)
  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1 hunks)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Clients/src/presentation/components/Charts/RiskHistoryChart.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-01-23T05:13:38.328Z
Learnt from: samuel-coutinho
Repo: bluewave-labs/verifywise PR: 524
File: Clients/src/presentation/pages/ProjectView/ProjectSettings/index.tsx:63-63
Timestamp: 2025-01-23T05:13:38.328Z
Learning: Always handle loading and error states in components that fetch data to prevent crashes and provide better UX. Use loading indicators during data fetching and display appropriate error messages when API calls fail.

Applied to files:

  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-08-07T21:26:04.614Z
Learnt from: Br0wnHammer
Repo: bluewave-labs/verifywise PR: 1872
File: Clients/src/application/hooks/useDashboard.ts:12-13
Timestamp: 2025-08-07T21:26:04.614Z
Learning: In the verifywise project, the `getAllEntities` function returns a response object where the actual data is nested within a `data` property, so the correct access pattern is `response.data` rather than accessing the response directly.

Applied to files:

  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.

Applied to files:

  • Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
🧬 Code graph analysis (1)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
  • getModelInventoryTimeseries (13-29)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx

[error] 204-204: TS2353: Object literal may only specify known properties, and 'padding' does not exist in type 'Partial<ChartsLegendProps | ContinuousColorLegendProps | PiecewiseColorLegendProps> & ChartsLegendPosition'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)

- Remove itemMarkWidth, itemMarkHeight, markGap, itemGap, and labelStyle
- These properties are not supported by MUI Charts legend type
- Use only direction and position properties which are type-safe
- Legend will use default MUI Charts styling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gorkem-bwl gorkem-bwl merged commit 7644167 into develop Nov 13, 2025
6 of 7 checks passed
@MuhammadKhalilzadeh MuhammadKhalilzadeh deleted the hp-nov-7-add-analytics branch November 22, 2025 16:35
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.

Add historical analytics tab for risks and models sections

4 participants