Skip to content

feat: calendar#774

Merged
coodos merged 7 commits intomainfrom
feat/calendar
Feb 5, 2026
Merged

feat: calendar#774
coodos merged 7 commits intomainfrom
feat/calendar

Conversation

@coodos
Copy link
Contributor

@coodos coodos commented Feb 5, 2026

Description of change

Issue Number

Type of change

  • New (a change which implements a new feature)

How the change has been tested

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Full Calendar platform: Day/Week/Month views, responsive layout, event creation & management, inline event list and month/day highlights.
    • Modern auth flow: wallet-based login with QR/mobile support and real-time sign-in stream; persistent token-based session for authenticated actions.
    • Event sync: create, list, update, delete events from the app; client API and mock data generator for demos.
    • UI improvements: theme toggle (light/dark), color picker, date/time picker, toasts, keyboard & accessibility enhancements.
  • Documentation

    • Added user-facing README describing features and usage.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds a new calendar platform: an Express+TypeScript calendar-api with JWT/SSE auth and EVault-backed event CRUDs, plus a Next.js calendar frontend including UI library, contexts, hooks, utilities, configs, and a CalendarEvent JSON Schema.

Changes

Cohort / File(s) Summary
Calendar API server
platforms/calendar-api/package.json, platforms/calendar-api/tsconfig.json, platforms/calendar-api/src/constants.ts, platforms/calendar-api/src/controllers/AuthController.ts, platforms/calendar-api/src/controllers/EventsController.ts, platforms/calendar-api/src/middleware/auth.ts, platforms/calendar-api/src/services/EVaultService.ts, platforms/calendar-api/src/index.ts, platforms/calendar-api/src/utils/version.ts
New calendar-api project: package & TS config; in-memory one-time session store (5min TTL); AuthController (offer, SSE, login) with signature verification, JWT issuance; SSE per-session emitter; auth middleware; EventsController endpoints; EVaultService for platform token management and GraphQL event CRUD; version helpers.
Next.js calendar app scaffold
platforms/calendar/package.json, platforms/calendar/tsconfig.json, platforms/calendar/next.config.ts, platforms/calendar/tailwind.config.ts, platforms/calendar/postcss.config.mjs, platforms/calendar/eslint.config.mjs, platforms/calendar/.gitignore, platforms/calendar/README.md, platforms/calendar/app/globals.css, platforms/calendar/app/layout.tsx, platforms/calendar/app/page.tsx
Project manifests and tool configs; global styles; root layout and page; env resolution helper; Tailwind/PostCSS/ESLint setup and repo-level ignore.
Client API & utilities
platforms/calendar/lib/calendar-api.ts, platforms/calendar/lib/mock-calendar-events.ts, platforms/calendar/lib/utils.ts
Browser-safe calendarApi client with token persistence and request wrapper; mock event generator; cn util (clsx + twMerge).
Auth, hooks, contexts
platforms/calendar/contexts/auth-context.tsx, platforms/calendar/hooks/use-mobile.tsx, platforms/calendar/hooks/use-toast.ts
AuthContext and useAuth with token persistence; mobile detection hook; in-module toast manager and useToast hook.
Calendar core components & demo
platforms/calendar/components/calendar/..., platforms/calendar/components/calendar/calendar-types.ts, platforms/calendar/components/calendar/calendar-context.tsx, platforms/calendar/components/calendar/calendar-provider.tsx, platforms/calendar/components/calendar/calendar.tsx, platforms/calendar/components/calendar/calendar-demo.tsx
Calendar types, context/provider, Calendar component and demo page wiring, event fetching/mapping and auth gating.
Views, layout, event primitives
platforms/calendar/components/calendar/body/..., platforms/calendar/components/calendar/calendar-event.tsx, platforms/calendar/components/calendar/calendar-body-*
Day/week/month views, headers, margins, CurrentTimeLine, event rendering with overlap/layout logic and animations.
Dialogs, forms, pickers
platforms/calendar/components/calendar/dialog/*, platforms/calendar/components/form/*, platforms/calendar/components/form/date-time-picker.tsx, platforms/calendar/components/form/color-picker.tsx
New-event and manage-event dialogs using react-hook-form + zod; DateTimePicker and ColorPicker UI components; API integration for create/update/delete.
Auth UI / Login
platforms/calendar/components/auth/login-screen.tsx, platforms/calendar/components/header/*
LoginScreen that fetches w3ds offer, parses session, opens SSE EventSource to receive token; header with theme toggle, GitHub link and sign-out handling.
UI component library
platforms/calendar/components/ui/* (many files)
Extensive set of reusable UI primitives and wrappers (Radix-based components, CVA variants, utility wrappers: accordion, dialog, buttons, selects, calendar, chart, carousel, sidebar, etc.).
Tooling & component config
platforms/calendar/components.json, platforms/calendar/postcss.config.mjs, platforms/calendar/tailwind.config.ts, platforms/calendar/eslint.config.mjs
shadcn/component config and build tooling configuration for styling and linting.
Ontology schema
services/ontology/schemas/calendarEvent.json
Adds CalendarEvent JSON Schema (title, color, start, end) with required fields and date-time formats.

Sequence Diagram(s)

%%{init: {"themeVariables": {}} }%%
sequenceDiagram
    participant Client
    participant AuthAPI as AuthController
    participant Session as SessionStore
    participant SSE as SSEEmitter
    participant Verifier as SignatureValidator
    participant JWT

    Client->>AuthAPI: GET /api/auth/offer
    AuthAPI->>Session: addSession(sessionId)
    AuthAPI-->>Client: w3ds URI with sessionId

    Client->>Client: user opens wallet and signs
    Client->>AuthAPI: GET /api/auth/sessions/:id (EventSource)
    AuthAPI->>SSE: subscribe(sessionId)
    SSE-->>Client: heartbeat / wait

    Client->>AuthAPI: POST /api/auth {ename, session, signature, appVersion}
    AuthAPI->>Session: isSessionValid(session)
    Session-->>AuthAPI: valid
    AuthAPI->>Verifier: verifySignature(signature, ename)
    Verifier-->>AuthAPI: valid
    AuthAPI->>JWT: sign({ename})
    JWT-->>AuthAPI: token
    AuthAPI->>SSE: emit token for sessionId
    SSE-->>Client: token
Loading
%%{init: {"themeVariables": {}} }%%
sequenceDiagram
    participant Client
    participant EventsAPI as EventsController
    participant AuthMW as authMiddleware
    participant EVault as EVaultService
    participant Platform as PlatformTokenEndpoint
    participant EVaultGQL as EVaultGraphQL

    Client->>EventsAPI: GET /api/events (Bearer token)
    EventsAPI->>AuthMW: validate token
    AuthMW-->>EventsAPI: req.user.ename
    EventsAPI->>EVault: listEvents(ename, first, after)
    EVault->>Platform: request platform token (cert client)
    Platform-->>EVault: platform token
    EVault->>EVaultGQL: GraphQL query (metaEnvelopes)
    EVaultGQL-->>EVault: events payload
    EVault-->>EventsAPI: mapped events
    EventsAPI-->>Client: 200 + events JSON
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through code and stitched a day,
Sessions, SSE, and JWT at play,
Events bloom from GraphQL streams,
Frontend petals, backend dreams,
A happy rabbit cheers—sync away!

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete. While the template structure is present, critical sections lack substantive information required for review. Complete the description by: (1) specifying the issue number if applicable; (2) detailing how the changes were tested (currently blank); (3) expanding the scope/objectives of the calendar feature beyond the template checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: calendar' is vague and generic, lacking specific detail about the primary change or feature being introduced. Clarify the title to describe the specific calendar functionality being added (e.g., 'feat: add calendar application with event management UI' or 'feat: implement calendar platform with authentication').

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/calendar

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

❤️ Share

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

Copy link
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: 11

🤖 Fix all issues with AI agents
In `@infrastructure/evault-core/src/core/protocol/graphql-server.ts`:
- Around line 889-895: Replace the hardcoded "maskedErrors: false" in the
GraphQL server configuration with an environment-driven value (e.g., use an
existing environment flag like process.env.NODE_ENV === 'production' or the
file's isProd/isDev helper) so that full errors are only exposed in
non-production environments; update the place where "maskedErrors" is set (the
current object containing maskedErrors and logging) to compute maskedErrors =
process.env.NODE_ENV === 'production' ? true : false (or the inverse depending
on the library expectation) and keep the logging object unchanged so behavior
follows the project's environment-based configuration pattern.

In `@platforms/calendar-api/src/controllers/AuthController.ts`:
- Around line 17-23: The handler uses process.env.PUBLIC_CALENDAR_BASE_URL
directly which can cause new URL(...) to throw; guard and validate baseUrl
before constructing redirectUri (the variables/functions to look for are
baseUrl, redirectUri, uuidv4, addSession, and res.json). Fix by checking that
baseUrl is present and a valid URL (e.g., if not present return a 500/400 JSON
error response after logging the issue), or wrap new URL(...) in a try/catch and
on error log the exception and return an appropriate error response instead of
proceeding to call addSession and res.json with an invalid redirectUri. Ensure
any early-return paths don’t call addSession or produce the success response.
- Around line 109-114: Remove the insecure fallback by requiring
process.env.JWT_SECRET in production (throw or process.exit if
NODE_ENV==='production' and JWT_SECRET is missing) and explicitly pass
algorithm: "HS256" to jwt.sign (where JWT_EXPIRES_IN and ename are used) in
AuthController (symbol jwt.sign, JWT_EXPIRES_IN, JWT_SECRET); mirror the same
behavior in the verification middleware (src/middleware/auth.ts) by rejecting
missing JWT_SECRET in production and adding algorithm: "HS256" to jwt.verify so
signing and verification are both explicit and fail fast when the secret is not
configured.

In `@platforms/calendar-api/src/controllers/EventsController.ts`:
- Around line 15-18: The current computation of first (variable in
EventsController.ts) uses Math.min(parseInt(... ) || 100, 500) which still
allows negative values; change it to parse the query into a number, default to
100 when parsing yields NaN, then clamp between 1 and 500 using Math.max and
Math.min (e.g. compute parsed = parseInt(String(req.query.first), 10); use
parsed = isNaN(parsed) ? 100 : parsed; then set first =
Math.min(Math.max(parsed, 1), 500)) so first is always a positive integer within
[1,500].

In `@platforms/calendar-api/src/index.ts`:
- Around line 30-37: The CORS setup using app.use(...) with cors(...) sets
origin: true while credentials: true, which is unsafe; update the cors
configuration used where app.use(...) calls cors to enforce a strict allowlist
by replacing origin: true with either a static array of allowed origins or an
origin callback that checks the request origin against an allowlist (and returns
the allowed origin or false), ensuring credentials remain enabled only for
matched origins; keep existing methods/allowedHeaders/credentials settings
otherwise.

In `@platforms/calendar-api/src/middleware/auth.ts`:
- Around line 23-35: Remove the hard-coded fallback secret and fail fast if
process.env.JWT_SECRET is not set (do not use "calendar-api-dev-secret"); in the
auth middleware where you extract token (const token = authHeader.slice(7))
check for a missing secret and return a 500 (or call next with an error) so the
server will not verify tokens with a known default; also make the jwt.verify
call (currently jwt.verify(token, secret) as AuthPayload) explicitly restrict
allowed algorithms by passing the options object (e.g., { algorithms: ['HS256']
}) to jwt.verify so only the intended HMAC algorithm(s) are accepted and the
verification cannot rely on library defaults. Ensure you still validate
decoded.ename and set req.user accordingly (same AuthPayload handling) after the
tightened verification.

In `@platforms/calendar-api/src/services/EVaultService.ts`:
- Around line 246-272: The two GraphQL calls using client.request (the
META_ENVELOPE_QUERY fetch and the UPDATE_MUTATION update) lack try/catch and
response-error handling; wrap both calls in try/catch similar to listEvents,
rethrow or throw a new Error containing the caught error details, and after the
UPDATE_MUTATION check result.updateMetaEnvelope.errors (and
result.updateMetaEnvelope.metaEnvelope null) and throw an aggregated Error with
those error messages so failures are surfaced; reference the
existing.metaEnvelope parsing logic, the payload construction, and use
META_ENVELOPE_QUERY and UPDATE_MUTATION identifiers when applying these checks.
- Around line 187-196: The mapping assumes result.metaEnvelopes is always
present; add a defensive null/undefined guard before using
result.metaEnvelopes.edges (inside the method in EVaultService.ts that returns
the mapped envelope list) — e.g., check if result.metaEnvelopes and
Array.isArray(result.metaEnvelopes.edges) and if not, return an empty array or
log and return empty; then proceed to map over result.metaEnvelopes.edges (and
keep using edge.node.parsed) so you never call .edges on null/undefined.
- Around line 99-133: ensurePlatformToken currently allows concurrent refreshes
and uses fetch without timeout; implement a simple in-process lock and a request
timeout: add a private Promise<string> | null field (e.g.,
platformTokenRefreshPromise) and modify ensurePlatformToken to check and await
that promise when a refresh is already in flight, so only one refresh sets
platformToken and tokenExpiresAt; when starting a refresh, assign a new promise
that performs the fetch and resolves/rejects, then clear the promise on
completion; also wrap the registry fetch call with a timeout (AbortController or
equivalent) and include abort handling so the request won’t hang indefinitely.
- Around line 204-220: The createMetaEnvelope GraphQL request in EVaultService
(the client.request call that returns createMetaEnvelope) lacks error handling;
wrap the client.request call inside a try-catch within the same method (e.g.,
the function containing the CREATE_MUTATION call), catch network/timeout
exceptions, log or rethrow a more informative error (including the caught
exception and context like payload.id/title), and still handle GraphQL-level
errors on the response (inspect result.createMetaEnvelope.errors and throw a
clear error if present) so callers receive consistent, contextual errors.

In `@platforms/calendar-api/src/utils/version.ts`:
- Around line 1-19: compareVersions currently maps parts with Number which
yields NaN for non-numeric segments like "1.2.3-alpha", causing incorrect 0
results and letting isVersionValid accept invalid versions; update
compareVersions to first validate each dot-separated segment with a strict
numeric check (e.g., /^\d+$/) for both version1 and version2, and if any segment
is non-numeric return -1 (or otherwise treat the version as older/invalid)
before numeric conversion and comparison; keep isVersionValid unchanged (it will
then correctly reject versions where compareVersions returns -1).
🧹 Nitpick comments (1)
platforms/calendar-api/src/constants.ts (1)

10-23: Prune expired sessions to avoid unbounded Map growth.
Expired sessions only clear on validation; idle sessions can accumulate. Consider pruning on add or a periodic sweep.

♻️ Proposed fix
 export const sessionStore = new Map<string, StoredSession>();
 
+function pruneExpiredSessions(): void {
+  const now = Date.now();
+  for (const [id, s] of sessionStore) {
+    if (now - s.createdAt > SESSION_TTL_MS) {
+      sessionStore.delete(id);
+    }
+  }
+}
+
 export function addSession(sessionId: string): void {
+  pruneExpiredSessions();
   sessionStore.set(sessionId, { createdAt: Date.now() });
 }

Copy link
Collaborator

@Bekiboo Bekiboo left a comment

Choose a reason for hiding this comment

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

lgtm

Copy link
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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@platforms/calendar-api/src/services/EVaultService.ts`:
- Around line 292-304: The removeEvent method currently calls
client.request(REMOVE_MUTATION, { id }) without a try-catch; wrap the request
call in a try-catch in removeEvent (the function that uses getClient and
REMOVE_MUTATION) so network/transport errors are caught and rethrown with
contextual information (include eName and id) while preserving the existing
logic that inspects result.removeMetaEnvelope.errors and throws their messages;
ensure the caught error is included in the new Error (or rethrown) so original
stack/message is retained.

In
`@platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx`:
- Around line 8-23: The current dayEvents filter uses isSameDay(event.start,
date) and thus drops events that start before midnight but continue into the
target day; update the filter in the CalendarBodyDayContent component to compute
the day's interval (use startOfDay(date) and endOfDay(date)) and use an
interval-overlap check (e.g., areIntervalsOverlapping from date-fns) between the
event interval (use event.start and event.end || event.start) and the day
interval; replace the existing events.filter(...) expression so CalendarEvent
still renders for any event that overlaps the current day.

In
`@platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx`:
- Around line 21-28: The dynamic Tailwind class `bg-${event.color}-500` in the
className for the event dot will be stripped by JIT; replace it by mapping
event.color to a static Tailwind class or falling back to a default, or use an
inline style when event.color is a CSS color string. Specifically, update the
div that currently uses `bg-${event.color}-500` (the size-2 rounded-full dot) to
use a colorMap lookup (e.g., map keys like "red", "blue" to "bg-red-500",
"bg-blue-500" and use colorMap[event.color] ?? 'bg-gray-500') or set style={{
backgroundColor: event.color }} if event.color is a valid CSS value; ensure the
chosen approach is applied where the dot div is rendered so Tailwind classes are
static or CSS values are used.

In `@platforms/calendar/components/calendar/calendar-event.tsx`:
- Around line 82-139: The dynamic Tailwind classes (e.g.,
bg-${event.color}-500/10, hover:bg-${event.color}-500/20,
border-${event.color}-500, text-${event.color}-500) must be replaced with static
class strings mapped from a safelist; import the existing colorOptions from
calendar-tailwind-classes.ts (or create a static map) and compute
bgClass/hoverClass/borderClass/textClass for the event color, then use those
variables in the className for the outer motion.div (previously using the
template literals) and the inner motion.div/p elements in calendar-event.tsx
(see the motion.div with layoutId and the nested motion.div with
layout="position"), and also update calendar-body-day-events.tsx where
bg-${event.color}-500 is used to reference the mapped static class instead so
Tailwind won’t purge the color classes in production.

In `@platforms/calendar/components/form/color-picker.tsx`:
- Around line 25-28: The dynamic Tailwind class `bg-${color.value}-500` will be
purged in production; replace the template literal with the pre-defined class
from your color option (use the existing `class.base` or a dedicated `bgClass`
on each entry in `colorOptions`) when building the className passed to `cn`
(i.e., use `color.class.base` or `color.bgClass` instead of
`bg-${color.value}-500`) so Tailwind can include the class at build time;
alternatively add those specific bg classes to the safelist in
tailwind.config.js if you prefer not to change the options.

In `@platforms/calendar/components/ui/carousel.tsx`:
- Around line 88-143: The carousel currently captures arrow keys from any
focused child via onKeyDownCapture and always preventsDefault; update the
behavior in handleKeyDown to only act when the carousel container itself is the
event target (i.e., event.target === event.currentTarget) or the container has
focus, map keys by orientation inside the handler (use ArrowLeft/ArrowRight for
horizontal and ArrowUp/ArrowDown for vertical based on the orientation or
opts.axis), and stop using capture so children (like inputs) keep normal
behavior; also ensure the container is focusable (set tabIndex=0) so it can
receive keyboard events.
- Around line 109-120: The effect in carousel.tsx attaches both api.on("reInit",
onSelect) and api.on("select", onSelect) but only removes the "select" listener;
update the cleanup to also remove the "reInit" listener by calling
api?.off("reInit", onSelect) (and keep api?.off("select", onSelect)) so the
reInit handler is not leaked when the effect re-runs or unmounts, ensuring the
api and onSelect references are guarded the same way they are when adding
listeners.

In `@platforms/calendar/components/ui/chart.tsx`:
- Around line 70-99: The ChartStyle component builds raw CSS in
dangerouslySetInnerHTML using id, config and keys from ChartConfig (see
ChartStyle, colorConfig, THEMES) which allows CSS injection; fix by escaping the
selector with CSS.escape(id) before interpolation, filter/validate each key used
in `--color-${key}` to a strict whitelist pattern (e.g. /^[A-Za-z0-9_-]+$/) and
validate color values (allow only hex, rgb(a), hsl(a) or CSS variable formats)
before including them, skipping any entries that fail validation so the
generated string only contains safe identifiers and colors.

In `@platforms/calendar/components/ui/collapsible.tsx`:
- Around line 7-9: The code assigns undefined by accessing non-existent
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent; update references to use the correct
exported symbols from `@radix-ui/react-collapsible` by replacing
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent with CollapsiblePrimitive.Trigger and
CollapsiblePrimitive.Content (or import Trigger and Content directly from the
package) so CollapsibleTrigger/CollapsibleContent variables reference the actual
Radix components.

In `@platforms/calendar/components/ui/form.tsx`:
- Around line 44-54: The null-check for fieldContext is currently after
fieldContext is used (fieldContext.name) which can throw; in useFormField move
the check "if (!fieldContext) throw new Error('useFormField should be used
within <FormField>')" to before any use of fieldContext (before calling
getFieldState or accessing fieldContext.name), so validate FormFieldContext
first, then call useFormContext/getFieldState and use
itemContext/FormItemContext as before.

In `@platforms/calendar/components/ui/progress.tsx`:
- Around line 11-23: The Progress component sets the visual indicator using the
prop value but fails to forward value to ProgressPrimitive.Root, causing ARIA to
treat it as indeterminate; update the component to pass the value prop into
ProgressPrimitive.Root (ensure the forwarded value is a number 0–100 or
undefined as appropriate) so ProgressPrimitive.Root receives
aria-valuenow/aria-valuemin/aria-valuemax that match the
ProgressPrimitive.Indicator; locate the fragment with ProgressPrimitive.Root and
ProgressPrimitive.Indicator and add the forwarded value prop to Root.

In `@platforms/calendar/package.json`:
- Around line 52-54: Update the react-day-picker dependency in package.json from
"react-day-picker": "8.10.1" to a React 19-compatible 9.x release (for example
"react-day-picker": "9.13.0") so the package's peerDependencies include React
19; modify the dependency string in package.json and run your package manager
install to lock the new version and resolve the peer dependency mismatch.
🟡 Minor comments (19)
platforms/calendar/README.md-33-35 (1)

33-35: ⚠️ Potential issue | 🟡 Minor

Hyphenate “full-month overview” and “quick-date selection.”

Both are compound adjectives modifying nouns.

✏️ Suggested edit
-- Full month overview
-- Quick date selection
+- Full-month overview
+- Quick-date selection
platforms/calendar/README.md-67-67 (1)

67-67: ⚠️ Potential issue | 🟡 Minor

Fix spelling: “Inspiration.”

The heading currently says “Inspiriation.”

✏️ Suggested edit
-## Original Inspiriation
+## Original Inspiration
platforms/calendar/components/ui/alert.tsx-35-44 (1)

35-44: ⚠️ Potential issue | 🟡 Minor

Type mismatch between ref type and rendered element.

AlertTitle has React.ElementRef typed as HTMLParagraphElement but renders an <h5> element (which is HTMLHeadingElement). This mismatch could cause issues when consumers use refs.

🐛 Proposed fix
 const AlertTitle = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLHeadingElement,
   React.HTMLAttributes<HTMLHeadingElement>
 >(({ className, ...props }, ref) => (
platforms/calendar/components/ui/alert.tsx-47-56 (1)

47-56: ⚠️ Potential issue | 🟡 Minor

Type mismatch: ref typed as HTMLParagraphElement but renders <div>.

The ref type should match the rendered element type.

🐛 Proposed fix
 const AlertDescription = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLDivElement,
-  React.HTMLAttributes<HTMLParagraphElement>
+  React.HTMLAttributes<HTMLDivElement>
 >(({ className, ...props }, ref) => (
platforms/calendar/components/calendar/body/month/calendar-body-month.tsx-39-46 (1)

39-46: ⚠️ Potential issue | 🟡 Minor

Multi-day events spanning the entire visible range are excluded.

Events that start before calendarStart and end after calendarEnd (i.e., span the entire month view) won't be included in visibleEvents because neither their start nor end falls within the interval.

🐛 Proposed fix
   const visibleEvents = events.filter(
     (event) =>
       isWithinInterval(event.start, {
         start: calendarStart,
         end: calendarEnd,
       }) ||
-      isWithinInterval(event.end, { start: calendarStart, end: calendarEnd })
+      isWithinInterval(event.end, { start: calendarStart, end: calendarEnd }) ||
+      (event.start <= calendarStart && event.end >= calendarEnd)
   )
platforms/calendar/components/auth/login-screen.tsx-42-63 (1)

42-63: ⚠️ Potential issue | 🟡 Minor

SSE error handling silently fails without user feedback.

When the EventSource encounters an error (line 59-61), it closes without notifying the user or attempting to reconnect. The user could be left waiting indefinitely with no indication that the connection failed.

🛡️ Suggested improvement to handle SSE failures
     eventSource.onerror = () => {
       eventSource.close()
+      // Optionally set error state or retry
+      setError('Connection lost. Please try again.')
     }

Alternatively, implement automatic retry with exponential backoff, or at minimum surface the error to the user so they can manually retry via fetchOffer.

platforms/calendar/components/ui/breadcrumb.tsx-105-105 (1)

105-105: ⚠️ Potential issue | 🟡 Minor

Typo in displayName: "BreadcrumbElipssis" should be "BreadcrumbEllipsis".

The displayName has a misspelling that will affect debugging in React DevTools.

🔧 Proposed fix
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
platforms/calendar/components/ui/menubar.tsx-217-217 (1)

217-217: ⚠️ Potential issue | 🟡 Minor

Typo: displayname should be displayName.

The property name uses a lowercase 'n', which breaks consistency with React's expected displayName convention and all other components in this file.

🔧 Proposed fix
-MenubarShortcut.displayname = "MenubarShortcut"
+MenubarShortcut.displayName = "MenubarShortcut"
platforms/calendar/components/calendar/header/date/calendar-header-date-badge.tsx-4-12 (1)

4-12: ⚠️ Potential issue | 🟡 Minor

Badge undercounts events spanning month boundaries.

The filter uses only event.start and misses multi-day events that began in a previous month but overlap the current month. Since event.end is always present (required in the CalendarEvent type), check for month overlap using both start and end dates.

✅ Recommended fix
-import { isSameMonth } from 'date-fns'
+import { startOfMonth, endOfMonth } from 'date-fns'

 export default function CalendarHeaderDateBadge() {
   const { events, date } = useCalendarContext()
-  const monthEvents = events.filter((event) => isSameMonth(event.start, date))
+  const monthStart = startOfMonth(date)
+  const monthEnd = endOfMonth(date)
+  const monthEvents = events.filter(
+    (event) => event.start <= monthEnd && event.end >= monthStart
+  )
platforms/calendar/components/calendar/calendar-provider.tsx-17-27 (1)

17-27: ⚠️ Potential issue | 🟡 Minor

calendarIconIsToday is required despite having a default value.

The prop type definition on line 24 declares calendarIconIsToday: boolean as required, but line 14 provides a default value. Callers will get a TypeScript error if they don't pass this prop.

🔧 Proposed fix
-  calendarIconIsToday: boolean
+  calendarIconIsToday?: boolean
platforms/calendar/components/form/date-time-picker.tsx-23-27 (1)

23-27: ⚠️ Potential issue | 🟡 Minor

Internal state doesn't sync with external field.value changes.

The component initializes date state from field.value only on mount. If field.value changes externally (e.g., form reset, parent state update), the picker will display stale data.

🔧 Proposed fix using useEffect to sync
 export function DateTimePicker({ field }: DateTimePickerProps) {
   const [date, setDate] = React.useState<Date>(
     field.value ? new Date(field.value) : new Date()
   )
   const [isOpen, setIsOpen] = React.useState(false)
+
+  React.useEffect(() => {
+    if (field.value) {
+      setDate(new Date(field.value))
+    }
+  }, [field.value])
platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx-46-54 (1)

46-54: ⚠️ Potential issue | 🟡 Minor

Schema/default value mismatch may cause validation issues.

The z.string().datetime() schema expects full ISO 8601 strings (e.g., "2024-01-15T14:30:00.000Z"), but defaultValues use format(date, "yyyy-MM-dd'T'HH:mm") which produces strings like "2024-01-15T14:30" without seconds or timezone.

While the DateTimePicker emits proper ISO strings after user interaction, submitting with untouched defaults could fail validation. The as any cast on the resolver may be masking this type incompatibility.

Proposed fix — use ISO string for defaults
     defaultValues: {
       title: '',
-      start: format(date, "yyyy-MM-dd'T'HH:mm"),
-      end: format(date, "yyyy-MM-dd'T'HH:mm"),
+      start: date.toISOString(),
+      end: date.toISOString(),
       color: 'blue',
     },
platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx-107-120 (1)

107-120: ⚠️ Potential issue | 🟡 Minor

Delete error not cleared on retry or close.

When handleDelete fails, submitError is set but never cleared if the user closes the dialog or retries. The error will persist across dialog reopenings since handleClose doesn't reset submitError.

Proposed fix — clear submitError in handleClose
   function handleClose() {
     setManageEventDialogOpen(false)
     setSelectedEvent(null)
+    setSubmitError(null)
     form.reset()
   }
platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx-69-78 (1)

69-78: ⚠️ Potential issue | 🟡 Minor

Same schema/default value mismatch as in CalendarNewEventDialog.

The format(selectedEvent.start, "yyyy-MM-dd'T'HH:mm") produces strings incompatible with z.string().datetime(). Consider using toISOString() instead for consistency with the schema.

Proposed fix
   useEffect(() => {
     if (selectedEvent) {
       form.reset({
         title: selectedEvent.title,
-        start: format(selectedEvent.start, "yyyy-MM-dd'T'HH:mm"),
-        end: format(selectedEvent.end, "yyyy-MM-dd'T'HH:mm"),
+        start: new Date(selectedEvent.start).toISOString(),
+        end: new Date(selectedEvent.end).toISOString(),
         color: selectedEvent.color,
       })
     }
   }, [selectedEvent, form])
platforms/calendar/components/ui/sidebar.tsx-441-443 (1)

441-443: ⚠️ Potential issue | 🟡 Minor

Typo in transition property: opa should be opacity.

The truncated property name transition-[margin,opa] will not properly animate opacity changes during collapse/expand.

🐛 Proposed fix
       className={cn(
-        "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
         "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
platforms/calendar/components/ui/sidebar.tsx-652-656 (1)

652-656: ⚠️ Potential issue | 🟡 Minor

Potential hydration mismatch with Math.random() in SSR context.

Using Math.random() inside useMemo with an empty dependency array will produce different values on the server vs. client during hydration, causing React hydration warnings and potential visual flickering.

🔧 Proposed fix using a stable seed or client-only rendering

Option 1: Use a stable default and only randomize on client:

 const SidebarMenuSkeleton = React.forwardRef<
   HTMLDivElement,
   React.ComponentProps<"div"> & {
     showIcon?: boolean
   }
 >(({ className, showIcon = false, ...props }, ref) => {
-  // Random width between 50 to 90%.
-  const width = React.useMemo(() => {
-    return `${Math.floor(Math.random() * 40) + 50}%`
-  }, [])
+  // Random width between 50 to 90%, generated on mount to avoid hydration mismatch
+  const [width, setWidth] = React.useState("70%")
+  React.useEffect(() => {
+    setWidth(`${Math.floor(Math.random() * 40) + 50}%`)
+  }, [])

Option 2: Accept width as a prop for deterministic rendering when needed.

platforms/calendar/components/ui/toast.tsx-73-87 (1)

73-87: ⚠️ Potential issue | 🟡 Minor

Add an accessible name to the icon-only close control.

The close button renders only an icon, so it should include an accessible label.

🔧 Suggested change
   <ToastPrimitives.Close
     ref={ref}
+    aria-label="Close"
     className={cn(
       "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
       className
     )}
platforms/calendar/components/ui/chart.tsx-299-310 (1)

299-310: ⚠️ Potential issue | 🟡 Minor

Legend label can render blank without a config label.
Consider falling back to the payload label/value so legend entries aren’t empty.

✅ Suggested fix
-              {itemConfig?.label}
+              {itemConfig?.label ?? item.value}
platforms/calendar/components/ui/chart.tsx-241-245 (1)

241-245: ⚠️ Potential issue | 🟡 Minor

Tooltip hides zero values.
item.value && (...) suppresses 0. Use an explicit null/undefined check so zeros render consistently with the formatter path above (line 102).

Suggested fix
-                      {item.value && (
+                      {item.value !== undefined && item.value !== null && (
                         <span className="font-mono font-medium tabular-nums text-foreground">
                           {item.value.toLocaleString()}
                         </span>
                       )}
🧹 Nitpick comments (28)
platforms/calendar/hooks/use-mobile.tsx (1)

1-19: Consider extracting to a shared utility to reduce duplication.

This hook is nearly identical to existing implementations in platforms/group-charter-manager/src/hooks/use-mobile.tsx, platforms/eReputation/client/src/hooks/use-mobile.tsx, and platforms/eVoting/src/hooks/use-mobile.tsx. If a shared hooks library exists (or is planned), consolidating these would reduce maintenance burden.

That said, the implementation is correct and consistent with the established codebase pattern—approve as-is if cross-platform extraction is out of scope for this PR.

platforms/calendar/components/ui/sheet.tsx (1)

24-24: Minor: Double space in className string.

There's an extra space between bg-black/80 and data-[state=open]. Not a functional issue, but worth cleaning up for consistency.

✨ Suggested fix
-      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+      "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
services/ontology/schemas/calendarEvent.json (2)

7-10: Consider adding minLength constraint for title.

The title property currently accepts an empty string, which may not be desirable for calendar events. Adding a minimum length constraint would enforce non-empty titles at validation time.

✨ Suggested improvement
     "title": {
       "type": "string",
-      "description": "Event title"
+      "description": "Event title",
+      "minLength": 1
     },

11-14: Consider adding pattern validation for hex color format.

The description mentions "hex or named" colors, but there's no validation. If hex colors are expected, a pattern could enforce the format while still allowing named colors.

✨ Optional pattern for hex color validation

If only hex colors should be allowed:

     "color": {
       "type": "string",
-      "description": "Event color (hex or named)"
+      "description": "Event color (hex format)",
+      "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$"
     },
platforms/calendar/components/ui/resizable.tsx (1)

23-43: Consider breaking the long className into multiple lines for readability.

The component logic is correct with proper accessibility support (focus-visible ring styles) and responsive handling for both horizontal and vertical panel directions. However, the single-line className on line 32 is difficult to read and maintain.

♻️ Suggested formatting for improved readability
 const ResizableHandle = ({
   withHandle,
   className,
   ...props
 }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
   withHandle?: boolean
 }) => (
   <ResizablePrimitive.PanelResizeHandle
     className={cn(
-      "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
+      "relative flex w-px items-center justify-center bg-border",
+      "after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2",
+      "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
+      "data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full",
+      "data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1",
+      "data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2",
+      "data-[panel-group-direction=vertical]:after:translate-x-0",
+      "[&[data-panel-group-direction=vertical]>div]:rotate-90",
       className
     )}
     {...props}
   >
platforms/calendar/components/ui/navigation-menu.tsx (1)

43-63: Minor: Duplicate group class.

The "group" class is already included in navigationMenuTriggerStyle (line 44), so adding it again on line 53 is redundant. Tailwind handles this gracefully, but removing the duplicate improves clarity.

♻️ Suggested fix
   <NavigationMenuPrimitive.Trigger
     ref={ref}
-    className={cn(navigationMenuTriggerStyle(), "group", className)}
+    className={cn(navigationMenuTriggerStyle(), className)}
     {...props}
   >
platforms/calendar/components/ui/progress.tsx (1)

11-23: Clamp value before computing translateX to avoid overshoot.

A defensive clamp prevents negative or >100 values from pushing the indicator outside bounds. This is optional but improves robustness.

♻️ Optional refactor
-const Progress = React.forwardRef<
+const Progress = React.forwardRef<
   React.ElementRef<typeof ProgressPrimitive.Root>,
   React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
->(({ className, value, ...props }, ref) => (
-  <ProgressPrimitive.Root
-    ref={ref}
-    className={cn(
-      "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
-      className
-    )}
-    {...props}
-  >
-    <ProgressPrimitive.Indicator
-      className="h-full w-full flex-1 bg-primary transition-all"
-      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
-    />
-  </ProgressPrimitive.Root>
-))
+>(({ className, value, ...props }, ref) => {
+  const clampedValue = Math.min(100, Math.max(0, value ?? 0))
+  return (
+    <ProgressPrimitive.Root
+      ref={ref}
+      className={cn(
+        "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
+        className
+      )}
+      {...props}
+    >
+      <ProgressPrimitive.Indicator
+        className="h-full w-full flex-1 bg-primary transition-all"
+        style={{ transform: `translateX(-${100 - clampedValue}%)` }}
+      />
+    </ProgressPrimitive.Root>
+  )
+})
platforms/calendar/package.json (1)

64-73: Consider pinning TypeScript and Node types versions more precisely.

Using "typescript": "^5" and "@types/node": "^20" allows a very wide range of versions. This could lead to inconsistent builds across environments. Consider using more specific version ranges (e.g., "^5.3.0") for better reproducibility.

platforms/calendar/tsconfig.json (1)

21-23: Make the @/ alias explicit with baseUrl.*

Some TS tooling expects baseUrl when paths are set; adding it avoids editor/tsserver drift.

♻️ Suggested tweak
     "jsx": "preserve",
     "incremental": true,
+    "baseUrl": ".",
     "plugins": [
       {
         "name": "next"
       }
     ],
platforms/calendar/components/calendar/calendar-mode-icon-map.tsx (1)

1-7: Prefer explicit ReactNode import to avoid relying on the global React namespace.

Some TS configs disable the global React namespace with the new JSX transform. Importing the type keeps this file resilient across configs.

🔧 Suggested tweak
+import type { ReactNode } from 'react'
 import { Columns2, Grid3X3, List } from 'lucide-react'
 import { Mode } from './calendar-types'
 
-export const calendarModeIconMap: Record<Mode, React.ReactNode> = {
+export const calendarModeIconMap: Record<Mode, ReactNode> = {
   day: <List />,
   week: <Columns2 />,
   month: <Grid3X3 />,
 }
platforms/calendar/components/calendar/body/month/calendar-body-month.tsx (2)

74-76: Multi-day events only appear on their start date.

dayEvents filters by isSameDay(event.start, day), so a multi-day event won't render on subsequent days it spans. Consider using isWithinInterval to show events on all days they cover.

♻️ Proposed fix
-            const dayEvents = visibleEvents.filter((event) =>
-              isSameDay(event.start, day)
-            )
+            const dayEvents = visibleEvents.filter((event) =>
+              isWithinInterval(day, { start: event.start, end: event.end }) ||
+              isSameDay(event.start, day)
+            )

101-131: Inner AnimatePresence may be unnecessary.

The AnimatePresence on line 101 wraps children where only the "+N more" indicator has motion props. The CalendarEvent components don't appear to be motion elements. If exit animations aren't needed for the event list, consider removing this wrapper to simplify the component.

platforms/calendar/lib/calendar-api.ts (1)

53-54: Type assertion undefined as T bypasses type safety.

When the response is 204, returning undefined as T can cause runtime issues if the caller expects a concrete type. Currently only deleteEvent returns Promise<void>, which is correct, but this pattern is fragile for future changes.

♻️ Consider making the 204 handling explicit
-async function request<T>(
+async function request<T = void>(
   path: string,
   options: RequestInit = {}
 ): Promise<T> {
   // ... existing code ...
-  if (res.status === 204) return undefined as T;
+  if (res.status === 204) return undefined as unknown as T;
   return res.json();
 }

Alternatively, create a separate helper for void-returning endpoints or use method overloads to better express the contract.

platforms/calendar/components/auth/login-screen.tsx (1)

128-132: Consider removing target="_blank" for wallet deep links.

Using target="_blank" on a deep link URI (like eid://...) may interfere with proper app invocation on mobile devices. The browser typically handles deep links by opening the associated app directly; opening a new tab first can cause issues on some platforms.

♻️ Suggested change
             <Button asChild className="w-full" size="lg">
-              <a href={uri} target="_blank" rel="noopener noreferrer">
+              <a href={uri}>
                 Open in eID Wallet
               </a>
             </Button>
platforms/calendar/components/ui/breadcrumb.tsx (1)

7-13: Unused separator prop in Breadcrumb component.

The separator prop is declared in the type definition but is never used in the component body. Either remove it or pass it through to allow customization.

♻️ Option 1: Remove unused prop
 const Breadcrumb = React.forwardRef<
   HTMLElement,
-  React.ComponentPropsWithoutRef<"nav"> & {
-    separator?: React.ReactNode
-  }
+  React.ComponentPropsWithoutRef<"nav">
 >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
♻️ Option 2: Use via React Context for separators

If the intent is to allow a custom separator throughout the breadcrumb, consider using React Context to pass it down to BreadcrumbSeparator children.

platforms/calendar/components/header/header-github.tsx (1)

6-17: Consider adding accessibility attributes.

The button lacks an accessible label for screen readers. Since it only contains an icon, users relying on assistive technology won't know its purpose.

♿ Proposed fix to add aria-label
     <Button
       variant="outline"
       size="icon"
+      aria-label="View source on GitHub"
       onClick={() =>
         window.open('https://github.com/charlietlamb/calendar', '_blank')
       }
     >
       <Github />
     </Button>
platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx (1)

20-28: Consider memoizing hour labels to avoid repeated Date creation.

new Date().setHours(hour, 0, 0, 0) is called for each hour on every render. While not a significant performance issue for 24 items, the hour labels are static and could be computed once.

♻️ Optional: Pre-compute hour labels
+const hourLabels = hours.slice(1).reduce(
+  (acc, hour) => {
+    const d = new Date()
+    d.setHours(hour, 0, 0, 0)
+    acc[hour] = format(d, 'h a')
+    return acc
+  },
+  {} as Record<number, string>
+)
+
 export default function CalendarBodyMarginDayMargin({
   className,
 }: {
   className?: string
 }) {
   return (
     <div
       className={cn(
         'sticky left-0 w-12 bg-background z-10 flex flex-col',
         className
       )}
     >
       <div className="sticky top-0 left-0 h-[33px] bg-background z-20 border-b" />
       <div className="sticky left-0 w-12 bg-background z-10 flex flex-col">
         {hours.map((hour) => (
           <div key={hour} className="relative h-32 first:mt-0">
             {hour !== 0 && (
               <span className="absolute text-xs text-muted-foreground -top-2.5 left-2">
-                {format(new Date().setHours(hour, 0, 0, 0), 'h a')}
+                {hourLabels[hour]}
               </span>
             )}
           </div>
         ))}
       </div>
     </div>
   )
 }
platforms/calendar/components/calendar/header/date/calendar-header-date-chevrons.tsx (2)

17-29: Consider adding a default case to the switch statement.

If mode has an unexpected value, the function silently does nothing. Adding a default case (even if it just logs a warning or throws) would help catch bugs during development.

🔧 Proposed fix
   function handleDateBackward() {
     switch (mode) {
       case 'month':
         setDate(subMonths(date, 1))
         break
       case 'week':
         setDate(subWeeks(date, 1))
         break
       case 'day':
         setDate(subDays(date, 1))
         break
+      default:
+        console.warn(`Unhandled calendar mode: ${mode}`)
     }
   }

47-53: Add aria-label attributes to navigation buttons for accessibility.

Screen reader users won't understand what these buttons do without accessible labels.

♿ Proposed fix
       <Button
         variant="outline"
         className="h-7 w-7 p-1"
         onClick={handleDateBackward}
+        aria-label="Previous"
       >
         <ChevronLeft className="min-w-5 min-h-5" />
       </Button>
       <Button
         variant="outline"
         className="h-7 w-7 p-1"
         onClick={handleDateForward}
+        aria-label="Next"
       >
         <ChevronRight className="min-w-5 min-h-5" />
       </Button>

Also applies to: 59-65

platforms/calendar/app/globals.css (1)

5-7: Consider aligning body font with the Geist variable (if intended).

The hard-coded Arial may override your intended font tokens. If --font-geist-sans is available, consider using it with fallbacks.

♻️ Example tweak
-body {
-  font-family: Arial, Helvetica, sans-serif;
-}
+body {
+  font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
+}
platforms/calendar/components/calendar-demo.tsx (1)

10-24: Consider validating date strings in mapApiToEvent.

The function creates Date objects from strings without validation. If the API returns invalid date strings, new Date(e.start) will produce an invalid Date object rather than throwing an error, which could cause silent failures in the calendar rendering.

🛡️ Optional validation
 function mapApiToEvent(e: {
   id: string
   title: string
   color: string
   start: string
   end: string
 }): CalendarEvent {
+  const start = new Date(e.start)
+  const end = new Date(e.end)
+  if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+    throw new Error(`Invalid date in event ${e.id}`)
+  }
   return {
     id: e.id,
     title: e.title,
     color: e.color || 'blue',
-    start: new Date(e.start),
-    end: new Date(e.end),
+    start,
+    end,
   }
 }
platforms/calendar/components/form/date-time-picker.tsx (1)

71-81: Unreachable condition: !date is always false.

Since date is initialized to new Date() on line 24-26 and is never set to a falsy value, the condition !date on line 73 will never be true. The muted foreground style and placeholder text are unreachable.

♻️ Consider using undefined for unset state or remove dead code

If an unset state is needed:

-  const [date, setDate] = React.useState<Date>(
+  const [date, setDate] = React.useState<Date | undefined>(
     field.value ? new Date(field.value) : new Date()
   )

Or if unset state isn't needed, remove the dead code:

         <Button
           variant="outline"
-          className={cn(
-            'w-full justify-start text-left font-normal',
-            !date && 'text-muted-foreground'
-          )}
+          className="w-full justify-start text-left font-normal"
         >
           <CalendarIcon className="mr-2 h-4 w-4" />
-          {date ? (
-            format(date, 'MM/dd/yyyy hh:mm aa')
-          ) : (
-            <span>MM/DD/YYYY hh:mm aa</span>
-          )}
+          {format(date, 'MM/dd/yyyy hh:mm aa')}
         </Button>
platforms/calendar/components/calendar/calendar-provider.tsx (1)

32-55: Consider memoizing the context value to prevent unnecessary re-renders.

The context value object is created inline on every render, causing all context consumers to re-render even when the actual values haven't changed.

♻️ Proposed optimization using useMemo
+import { useState, useMemo } from 'react'
-import { useState } from 'react'

 // ... inside the component:
+  const contextValue = useMemo(
+    () => ({
+      events,
+      setEvents,
+      mode,
+      setMode,
+      date,
+      setDate,
+      calendarIconIsToday,
+      newEventDialogOpen,
+      setNewEventDialogOpen,
+      manageEventDialogOpen,
+      setManageEventDialogOpen,
+      selectedEvent,
+      setSelectedEvent,
+      refetchEvents,
+    }),
+    [
+      events,
+      setEvents,
+      mode,
+      setMode,
+      date,
+      setDate,
+      calendarIconIsToday,
+      newEventDialogOpen,
+      manageEventDialogOpen,
+      selectedEvent,
+      refetchEvents,
+    ]
+  )

   return (
-    <CalendarContext.Provider
-      value={{
-        events,
-        setEvents,
-        // ... all the values
-      }}
-    >
+    <CalendarContext.Provider value={contextValue}>
platforms/calendar/lib/mock-calendar-events.ts (1)

26-30: getRandomTime mutates its input parameter.

The function mutates the passed date object via setHours(). While this works in the current usage since eventDate is a fresh Date from addDays(), it's a subtle side effect that could cause issues if the function is reused elsewhere.

♻️ Proposed fix to avoid mutation
 function getRandomTime(date: Date): Date {
   const hours = Math.floor(Math.random() * 14) + 8 // 8 AM to 9 PM
   const minutes = Math.floor(Math.random() * 4) * 15 // 0, 15, 30, 45
-  return new Date(date.setHours(hours, minutes, 0, 0))
+  const result = new Date(date)
+  result.setHours(hours, minutes, 0, 0)
+  return result
 }

Also, the comment says "8 AM to 10 PM" but the range is actually 8 AM to 9 PM (hours 8-21).

platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx (1)

40-40: Unused variable setEvents destructured from context.

setEvents is imported from context but never used in this component. Consider removing it to keep the code clean.

Proposed fix
   const {
     newEventDialogOpen,
     setNewEventDialogOpen,
     date,
-    setEvents,
     refetchEvents,
   } = useCalendarContext()
platforms/calendar/components/ui/sidebar.tsx (1)

86-88: Consider adding explicit SameSite attribute to the cookie.

While modern browsers default to SameSite=Lax, being explicit improves clarity and ensures consistent behavior across all browsers.

♻️ Suggested improvement
-        document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+        document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax`
platforms/calendar/components/ui/sonner.tsx (1)

3-6: Prefer explicit React type imports for ComponentProps.

React.ComponentProps relies on the React namespace being globally available; in TS configs that disallow UMD globals this can fail. Importing the type directly is safer in ESM modules.

🔧 Suggested change
 import { useTheme } from "next-themes"
 import { Toaster as Sonner } from "sonner"
+import type { ComponentProps } from "react"

-type ToasterProps = React.ComponentProps<typeof Sonner>
+type ToasterProps = ComponentProps<typeof Sonner>
platforms/calendar/hooks/use-toast.ts (1)

174-185: Avoid re-subscribing the listener on every state update.

The effect depends on state, so it tears down and re-adds the same listener on each toast update. setState is stable, so you can subscribe once.

🔧 Suggested change
-  }, [state])
+  }, [])

Comment on lines +292 to +304
async removeEvent(eName: string, id: string): Promise<boolean> {
const client = await this.getClient(eName);
const result = await client.request<{
removeMetaEnvelope: { success: boolean; errors: Array<{ message: string }> };
}>(REMOVE_MUTATION, { id });

if (result.removeMetaEnvelope.errors?.length) {
throw new Error(
result.removeMetaEnvelope.errors.map((e) => e.message).join("; ")
);
}
return result.removeMetaEnvelope.success;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Missing try-catch for removeEvent GraphQL request.

For consistency with listEvents, wrap the client.request call in a try-catch to provide contextual error messages for network failures.

🛡️ Proposed fix
   async removeEvent(eName: string, id: string): Promise<boolean> {
     const client = await this.getClient(eName);
+    let result;
+    try {
-    const result = await client.request<{
+      result = await client.request<{
       removeMetaEnvelope: { success: boolean; errors: Array<{ message: string }> };
     }>(REMOVE_MUTATION, { id });
+    } catch (err: unknown) {
+      const msg = err instanceof Error ? err.message : String(err);
+      throw new Error(`Failed to remove event ${id}: ${msg}`);
+    }

     if (result.removeMetaEnvelope.errors?.length) {
🤖 Prompt for AI Agents
In `@platforms/calendar-api/src/services/EVaultService.ts` around lines 292 - 304,
The removeEvent method currently calls client.request(REMOVE_MUTATION, { id })
without a try-catch; wrap the request call in a try-catch in removeEvent (the
function that uses getClient and REMOVE_MUTATION) so network/transport errors
are caught and rethrown with contextual information (include eName and id) while
preserving the existing logic that inspects result.removeMetaEnvelope.errors and
throws their messages; ensure the caught error is included in the new Error (or
rethrown) so original stack/message is retained.

Comment on lines 8 to 23
const { events } = useCalendarContext()

const dayEvents = events.filter((event) => isSameDay(event.start, date))

return (
<div className="flex flex-col flex-grow">
<CalendarBodyHeader date={date} />

<div className="flex-1 relative">
{hours.map((hour) => (
<div key={hour} className="h-32 border-b border-border/50 group" />
))}

{dayEvents.map((event) => (
<CalendarEvent key={event.id} event={event} />
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Day view drops events that span midnight.

Filtering only by event.start misses events that began the previous day but overlap the current day. Consider filtering by overlap with the day interval instead.

🛠️ Suggested fix (interval overlap)
-  const dayEvents = events.filter((event) => isSameDay(event.start, date))
+  const dayStart = new Date(date)
+  dayStart.setHours(0, 0, 0, 0)
+  const dayEnd = new Date(date)
+  dayEnd.setHours(23, 59, 59, 999)
+
+  const dayEvents = events.filter(
+    (event) => event.start < dayEnd && event.end > dayStart
+  )
🤖 Prompt for AI Agents
In
`@platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx`
around lines 8 - 23, The current dayEvents filter uses isSameDay(event.start,
date) and thus drops events that start before midnight but continue into the
target day; update the filter in the CalendarBodyDayContent component to compute
the day's interval (use startOfDay(date) and endOfDay(date)) and use an
interval-overlap check (e.g., areIntervalsOverlapping from date-fns) between the
event interval (use event.start and event.end || event.start) and the day
interval; replace the existing events.filter(...) expression so CalendarEvent
still renders for any event that overlaps the current day.

Comment on lines +21 to +28
>
<div className="flex items-center gap-2">
<div className={`size-2 rounded-full bg-${event.color}-500`} />
<p className="text-muted-foreground text-sm font-medium">
{event.title}
</p>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dynamic Tailwind class will not work with JIT compilation.

The interpolated class bg-${event.color}-500 won't be included in the production build because Tailwind's JIT compiler cannot detect dynamically constructed class names. The color dot will have no background color.

🐛 Proposed fix using a color mapping or inline style

Option 1: Use inline style (simplest)

-            <div className={`size-2 rounded-full bg-${event.color}-500`} />
+            <div
+              className="size-2 rounded-full"
+              style={{ backgroundColor: event.color }}
+            />

This requires event.color to be a valid CSS color value (e.g., #3b82f6 instead of blue).

Option 2: Use a safelist or color map

const colorMap: Record<string, string> = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
  green: 'bg-green-500',
  // ... other colors
}

// Then use:
<div className={`size-2 rounded-full ${colorMap[event.color] ?? 'bg-gray-500'}`} />
📝 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
>
<div className="flex items-center gap-2">
<div className={`size-2 rounded-full bg-${event.color}-500`} />
<p className="text-muted-foreground text-sm font-medium">
{event.title}
</p>
</div>
</div>
>
<div className="flex items-center gap-2">
<div
className="size-2 rounded-full"
style={{ backgroundColor: event.color }}
/>
<p className="text-muted-foreground text-sm font-medium">
{event.title}
</p>
</div>
</div>
🤖 Prompt for AI Agents
In `@platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx`
around lines 21 - 28, The dynamic Tailwind class `bg-${event.color}-500` in the
className for the event dot will be stripped by JIT; replace it by mapping
event.color to a static Tailwind class or falling back to a default, or use an
inline style when event.color is a CSS color string. Specifically, update the
div that currently uses `bg-${event.color}-500` (the size-2 rounded-full dot) to
use a colorMap lookup (e.g., map keys like "red", "blue" to "bg-red-500",
"bg-blue-500" and use colorMap[event.color] ?? 'bg-gray-500') or set style={{
backgroundColor: event.color }} if event.color is a valid CSS value; ensure the
chosen approach is applied where the dot div is rendered so Tailwind classes are
static or CSS values are used.

Comment on lines +82 to +139
return (
<MotionConfig reducedMotion="user">
<AnimatePresence mode="wait">
<motion.div
className={cn(
`px-3 py-1.5 rounded-md truncate cursor-pointer transition-all duration-300 bg-${event.color}-500/10 hover:bg-${event.color}-500/20 border border-${event.color}-500`,
!month && 'absolute',
className
)}
style={style}
onClick={(e) => {
e.stopPropagation()
setSelectedEvent(event)
setManageEventDialogOpen(true)
}}
initial={{
opacity: 0,
y: -3,
scale: 0.98,
}}
animate={{
opacity: 1,
y: 0,
scale: 1,
}}
exit={{
opacity: 0,
scale: 0.98,
transition: {
duration: 0.15,
ease: 'easeOut',
},
}}
transition={{
duration: 0.2,
ease: [0.25, 0.1, 0.25, 1],
opacity: {
duration: 0.2,
ease: 'linear',
},
layout: {
duration: 0.2,
ease: 'easeOut',
},
}}
layoutId={`event-${animationKey}-${month ? 'month' : 'day'}`}
>
<motion.div
className={cn(
`flex flex-col w-full text-${event.color}-500`,
month && 'flex-row items-center justify-between'
)}
layout="position"
>
<p className={cn('font-bold truncate', month && 'text-xs')}>
{event.title}
</p>
<p className={cn('text-sm', month && 'text-xs')}>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find Tailwind configuration files
find . -maxdepth 2 -type f \( -name "tailwind.config.*" -o -name "postcss.config.*" \) 2>/dev/null

Repository: MetaState-Prototype-Project/prototype

Length of output: 63


🏁 Script executed:

# Check the actual file to see the current implementation
cat -n platforms/calendar/components/calendar/calendar-event.tsx | sed -n '70,150p'

Repository: MetaState-Prototype-Project/prototype

Length of output: 3180


🏁 Script executed:

# Search for any safelist configuration in config files
rg -t ts -t js "safelist|content" --max-count=20 2>/dev/null | head -50

Repository: MetaState-Prototype-Project/prototype

Length of output: 12129


🏁 Script executed:

# Search for tailwind config in the calendar platform
find platforms/calendar -name "tailwind.config.*" -o -name "postcss.config.*" 2>/dev/null

Repository: MetaState-Prototype-Project/prototype

Length of output: 156


🏁 Script executed:

# Check for any tailwind config at repo root or common locations
find . -maxdepth 3 -name "tailwind.config.*" 2>/dev/null

Repository: MetaState-Prototype-Project/prototype

Length of output: 364


🏁 Script executed:

# Search for safelist in all tailwind configs
rg -t ts "safelist" --max-count=50 2>/dev/null

Repository: MetaState-Prototype-Project/prototype

Length of output: 63


🏁 Script executed:

# Check the calendar platform structure
ls -la platforms/calendar/ 2>/dev/null | head -20

Repository: MetaState-Prototype-Project/prototype

Length of output: 1120


🏁 Script executed:

# Check the calendar tailwind config
cat -n platforms/calendar/tailwind.config.ts

Repository: MetaState-Prototype-Project/prototype

Length of output: 3696


🏁 Script executed:

# Also check postcss.config
cat -n platforms/calendar/postcss.config.mjs

Repository: MetaState-Prototype-Project/prototype

Length of output: 271


🏁 Script executed:

# Check what color values are possible for event.color
rg -t ts "event\.color|color:" platforms/calendar --max-count=30

Repository: MetaState-Prototype-Project/prototype

Length of output: 1885


🏁 Script executed:

# Look for the event type definition
rg -t ts "type.*[Ee]vent|interface.*[Ee]vent" platforms/calendar --max-count=20 -A 5

Repository: MetaState-Prototype-Project/prototype

Length of output: 1317


🏁 Script executed:

# Check the EVENT_COLORS definition
cat -n platforms/calendar/lib/mock-calendar-events.ts | head -30

Repository: MetaState-Prototype-Project/prototype

Length of output: 1160


🏁 Script executed:

# Look for EVENT_COLORS definition
rg -t ts "EVENT_COLORS" platforms/calendar -B 2 -A 10

Repository: MetaState-Prototype-Project/prototype

Length of output: 2055


🏁 Script executed:

# Check if there's a color validation in the dialogs
cat -n platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx | head -50

Repository: MetaState-Prototype-Project/prototype

Length of output: 1716


🏁 Script executed:

# Check the calendar-tailwind-classes file which contains colorOptions
cat -n platforms/calendar/components/calendar/calendar-tailwind-classes.ts

Repository: MetaState-Prototype-Project/prototype

Length of output: 3053


🏁 Script executed:

# Check for all uses of dynamic bg- classes with event.color
rg -t ts "bg-\$\{event\.color\}|text-\$\{event\.color\}" platforms/calendar

Repository: MetaState-Prototype-Project/prototype

Length of output: 63


🏁 Script executed:

# Verify how colorOptions is currently being used
rg -t ts "colorOptions" platforms/calendar -B 2 -A 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 2297


🏁 Script executed:

# Check how calendar-body-day-events uses colors
cat -n platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx | grep -A 3 -B 3 "event.color"

Repository: MetaState-Prototype-Project/prototype

Length of output: 409


🏁 Script executed:

# Check if there are any other usages of dynamic event.color in className
rg -t ts "event\.color" platforms/calendar --max-count=30

Repository: MetaState-Prototype-Project/prototype

Length of output: 592


Use static color classes instead of dynamic template strings — colors will be purged in production.

The Tailwind content scanner doesn't pick up dynamic template literals like bg-${event.color}-500/10. Without a safelist, these classes will be removed in production builds. Refactor to use a static class map or the existing colorOptions in calendar-tailwind-classes.ts.

Affected locations:

  • calendar-event.tsx lines 87 and 131: bg-${event.color}-500/10, hover:bg-${event.color}-500/20, border-${event.color}-500, text-${event.color}-500
  • calendar-body-day-events.tsx line 23: bg-${event.color}-500
✅ Option 1: Use existing colorOptions

Import colorOptions from calendar-tailwind-classes.ts and map event.color to the corresponding class string.

+import { colorOptions } from '../calendar-tailwind-classes'
+
+function getColorClasses(color: string) {
+  return colorOptions.find(opt => opt.value === color)?.class.base ?? colorOptions[0].class.base
+}

Then use:

-className={cn(
-  `px-3 py-1.5 rounded-md truncate cursor-pointer transition-all duration-300 bg-${event.color}-500/10 hover:bg-${event.color}-500/20 border border-${event.color}-500`,
-  !month && 'absolute',
-  className
-)}
+className={cn(
+  'px-3 py-1.5 rounded-md truncate cursor-pointer transition-all duration-300 border',
+  getColorClasses(event.color),
+  !month && 'absolute',
+  className
+)}
✅ Option 2: Static class map
+const EVENT_COLOR_CLASS: Record<string, { container: string; text: string }> = {
+  blue: {
+    container: 'bg-blue-500/10 hover:bg-blue-500/20 border-blue-500',
+    text: 'text-blue-500',
+  },
+  indigo: {
+    container: 'bg-indigo-500/10 hover:bg-indigo-500/20 border-indigo-500',
+    text: 'text-indigo-500',
+  },
+  pink: {
+    container: 'bg-pink-500/10 hover:bg-pink-500/20 border-pink-500',
+    text: 'text-pink-500',
+  },
+  red: {
+    container: 'bg-red-500/10 hover:bg-red-500/20 border-red-500',
+    text: 'text-red-500',
+  },
+  orange: {
+    container: 'bg-orange-500/10 hover:bg-orange-500/20 border-orange-500',
+    text: 'text-orange-500',
+  },
+  amber: {
+    container: 'bg-amber-500/10 hover:bg-amber-500/20 border-amber-500',
+    text: 'text-amber-500',
+  },
+  emerald: {
+    container: 'bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500',
+    text: 'text-emerald-500',
+  },
+}
🤖 Prompt for AI Agents
In `@platforms/calendar/components/calendar/calendar-event.tsx` around lines 82 -
139, The dynamic Tailwind classes (e.g., bg-${event.color}-500/10,
hover:bg-${event.color}-500/20, border-${event.color}-500,
text-${event.color}-500) must be replaced with static class strings mapped from
a safelist; import the existing colorOptions from calendar-tailwind-classes.ts
(or create a static map) and compute bgClass/hoverClass/borderClass/textClass
for the event color, then use those variables in the className for the outer
motion.div (previously using the template literals) and the inner motion.div/p
elements in calendar-event.tsx (see the motion.div with layoutId and the nested
motion.div with layout="position"), and also update calendar-body-day-events.tsx
where bg-${event.color}-500 is used to reference the mapped static class instead
so Tailwind won’t purge the color classes in production.

Comment on lines +25 to +28
className={cn(
'size-6 border-0 shadow-none transition-all duration-200',
`bg-${color.value}-500`
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dynamic Tailwind class names won't work in production.

Tailwind's JIT compiler purges classes at build time via static analysis. The template literal `bg-${color.value}-500` is constructed dynamically, so these classes won't be included in the production CSS bundle.

The colorOptions array already provides pre-defined class strings in the class.base property—use those instead.

🐛 Proposed fix
           className={cn(
             'size-6 border-0 shadow-none transition-all duration-200',
-            `bg-${color.value}-500`
+            color.class.base.split(' ').find(c => c.startsWith('bg-') && c.includes('500'))
           )}

Or more simply, extract just the background color class from colorOptions:

           className={cn(
             'size-6 border-0 shadow-none transition-all duration-200',
-            `bg-${color.value}-500`
+            `bg-${color.value}-500` // safelist in tailwind.config or use color.class.base
           )}

Alternative: Add a dedicated bgClass property to each color option, or safelist these classes in tailwind.config.js.

🤖 Prompt for AI Agents
In `@platforms/calendar/components/form/color-picker.tsx` around lines 25 - 28,
The dynamic Tailwind class `bg-${color.value}-500` will be purged in production;
replace the template literal with the pre-defined class from your color option
(use the existing `class.base` or a dedicated `bgClass` on each entry in
`colorOptions`) when building the className passed to `cn` (i.e., use
`color.class.base` or `color.bgClass` instead of `bg-${color.value}-500`) so
Tailwind can include the class at build time; alternatively add those specific
bg classes to the safelist in tailwind.config.js if you prefer not to change the
options.

Comment on lines +70 to +99
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)

if (!colorConfig.length) {
return null
}

return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n platforms/calendar/components/ui/chart.tsx | head -120

Repository: MetaState-Prototype-Project/prototype

Length of output: 4317


🏁 Script executed:

# Search for ChartStyle usage and how id/config are passed
rg "ChartStyle" --type tsx --type ts -B 3 -A 3

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

# Check the ChartConfig type definition
rg "type ChartConfig|interface ChartConfig" --type tsx --type ts -A 10

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

# Search for where this component is used and how id parameter is set
rg "data-chart" --type tsx --type ts -B 5 -A 5

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

# Search for ChartContainer usage without type filter
rg "ChartContainer" -B 2 -A 5

Repository: MetaState-Prototype-Project/prototype

Length of output: 11252


🏁 Script executed:

# Search for data-chart attribute usage
rg "data-chart" -B 3 -A 3

Repository: MetaState-Prototype-Project/prototype

Length of output: 8858


🏁 Script executed:

# Check if config is ever populated from user data or external sources
rg "ChartConfig" -B 2 -A 5 | head -100

Repository: MetaState-Prototype-Project/prototype

Length of output: 7179


Escape CSS selector and validate color values in dangerouslySetInnerHTML.
The id and config parameters are interpolated directly into CSS without sanitization. If either can be user-controlled, this enables CSS injection attacks. Use CSS.escape() for the selector and validate color/key values before injection.

🛡️ Proposed hardening for CSS injection
 const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+  const safeId = CSS.escape(id)
+  const isSafeKey = (value: string) => /^[A-Za-z0-9_-]+$/.test(value)
+  const isSafeColor = (value?: string) =>
+    !!value &&
+    (/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i.test(value) ||
+      /^[a-zA-Z]+$/.test(value) ||
+      /^rgb(a)?\(/i.test(value) ||
+      /^hsl(a)?\(/i.test(value) ||
+      /^var\(--[-\w]+\)$/.test(value))
   const colorConfig = Object.entries(config).filter(
     ([, config]) => config.theme || config.color
   )
@@
-            ([theme, prefix]) => `
+            ([theme, prefix]) => `
 ${prefix} [data-chart=${id}] {
 ${colorConfig
   .map(([key, itemConfig]) => {
+    if (!isSafeKey(key)) return null
     const color =
       itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
       itemConfig.color
-    return color ? `  --color-${key}: ${color};` : null
+    const safeColor = isSafeColor(color) ? color : null
+    return safeColor ? `  --color-${key}: ${safeColor};` : null
   })
   .join("\n")}
 }
 `
           )
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 80-80: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In `@platforms/calendar/components/ui/chart.tsx` around lines 70 - 99, The
ChartStyle component builds raw CSS in dangerouslySetInnerHTML using id, config
and keys from ChartConfig (see ChartStyle, colorConfig, THEMES) which allows CSS
injection; fix by escaping the selector with CSS.escape(id) before
interpolation, filter/validate each key used in `--color-${key}` to a strict
whitelist pattern (e.g. /^[A-Za-z0-9_-]+$/) and validate color values (allow
only hex, rgb(a), hsl(a) or CSS variable formats) before including them,
skipping any entries that fail validation so the generated string only contains
safe identifiers and colors.

Comment on lines +7 to +9
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger

const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
Copy link
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

🏁 Script executed:

# Read the collapsible.tsx file
cat platforms/calendar/components/ui/collapsible.tsx

Repository: MetaState-Prototype-Project/prototype

Length of output: 409


🌐 Web query:

Radix UI react-collapsible 1.1.2 component exports API reference

💡 Result:

@radix-ui/react-collapsible@1.1.2 exports (public API surface)

Radix’s Collapsible package exposes these React components (commonly used as both namespaced and named exports):

Many ecosystems/tools also re-export/alias these as:

  • CollapsibleRoot
  • CollapsibleTriggerTrigger
  • CollapsibleContentContent (quadratic-ui.com)

Component API reference (props / attributes)

Root

  • Props: asChild, defaultOpen, open, onOpenChange, disabled
  • Data attributes: data-state="open|closed", data-disabled (radix-ui.com)

Trigger

  • Props: asChild
  • Data attributes: data-state="open|closed", data-disabled (radix-ui.com)

Content

  • Props: asChild, forceMount
  • Data attributes: data-state="open|closed", data-disabled
  • CSS vars: --radix-collapsible-content-width, --radix-collapsible-content-height (radix-ui.com)

Citations:


Incorrect Radix UI property access — components will be undefined.

The @radix-ui/react-collapsible package exports Trigger and Content, not CollapsibleTrigger and CollapsibleContent. Accessing non-existent properties on the namespace import will assign undefined to these constants, causing runtime errors when the components are rendered.

🐛 Proposed fix
-const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+const CollapsibleTrigger = CollapsiblePrimitive.Trigger

-const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+const CollapsibleContent = CollapsiblePrimitive.Content
🤖 Prompt for AI Agents
In `@platforms/calendar/components/ui/collapsible.tsx` around lines 7 - 9, The
code assigns undefined by accessing non-existent
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent; update references to use the correct
exported symbols from `@radix-ui/react-collapsible` by replacing
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent with CollapsiblePrimitive.Trigger and
CollapsiblePrimitive.Content (or import Trigger and Content directly from the
package) so CollapsibleTrigger/CollapsibleContent variables reference the actual
Radix components.

Comment on lines +44 to +54
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()

const fieldState = getFieldState(fieldContext.name, formState)

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: Null check occurs after fieldContext is already used.

On line 49, fieldContext.name is accessed before the null check on line 51. If useFormField is called outside a <FormField>, this will throw a TypeError when accessing .name on the empty object, rather than the intended helpful error message.

🐛 Proposed fix to move the check before usage
 const useFormField = () => {
   const fieldContext = React.useContext(FormFieldContext)
   const itemContext = React.useContext(FormItemContext)
+
+  if (!fieldContext.name) {
+    throw new Error("useFormField should be used within <FormField>")
+  }
+
   const { getFieldState, formState } = useFormContext()
 
   const fieldState = getFieldState(fieldContext.name, formState)
 
-  if (!fieldContext) {
-    throw new Error("useFormField should be used within <FormField>")
-  }
-
   const { id } = itemContext
🤖 Prompt for AI Agents
In `@platforms/calendar/components/ui/form.tsx` around lines 44 - 54, The
null-check for fieldContext is currently after fieldContext is used
(fieldContext.name) which can throw; in useFormField move the check "if
(!fieldContext) throw new Error('useFormField should be used within
<FormField>')" to before any use of fieldContext (before calling getFieldState
or accessing fieldContext.name), so validate FormFieldContext first, then call
useFormContext/getFieldState and use itemContext/FormItemContext as before.

Comment on lines +11 to +23
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "progress.tsx" -path "*/calendar/components/ui/*" | head -20

Repository: MetaState-Prototype-Project/prototype

Length of output: 128


🏁 Script executed:

cat -n platforms/calendar/components/ui/progress.tsx

Repository: MetaState-Prototype-Project/prototype

Length of output: 1068


🌐 Web query:

@radix-ui/react-progress ProgressPrimitive.Root value prop accessibility ARIA

💡 Result:

In @radix-ui/react-progress, ProgressPrimitive.Root uses the value prop to drive both state and ARIA:

  • value: number | null
    • If value is a number, the progressbar is determinate (data-state="loading" or "complete" when value === max). The root sets:
      • role="progressbar"
      • aria-valuemin="0"
      • aria-valuemax = max
      • aria-valuenow = value
      • aria-valuetext = label derived from getValueLabel (defaults to a percentage-style label). [1][2]
    • If value is null (or omitted), the progressbar is indeterminate (data-state="indeterminate"), and aria-valuenow should not be present (per the ARIA progressbar pattern). [1][2][3]

max is the maximum value used for aria-valuemax (commonly defaulting to 100). [2]

Sources: Radix Progress docs/API [1], Radix progress package docs/ARIA attributes summary [2], ARIA progressbar role guidance [3].

References:
[1] (radix-ui.com)
[2] (tessl.io)
[3] (docs.rodeo)

Citations:


Pass value to ProgressPrimitive.Root to ensure ARIA attributes match the visual indicator.

The component destructures value and uses it to control the indicator's visual progress, but doesn't pass it to the Root element. This causes a mismatch: the Root will be marked as indeterminate (missing aria-valuenow and other ARIA attributes), even though the UI displays a determinate progress bar.

Suggested fix
   <ProgressPrimitive.Root
     ref={ref}
+    value={value}
     className={cn(
       "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
       className
     )}
📝 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
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
value={value}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
🤖 Prompt for AI Agents
In `@platforms/calendar/components/ui/progress.tsx` around lines 11 - 23, The
Progress component sets the visual indicator using the prop value but fails to
forward value to ProgressPrimitive.Root, causing ARIA to treat it as
indeterminate; update the component to pass the value prop into
ProgressPrimitive.Root (ensure the forwarded value is a number 0–100 or
undefined as appropriate) so ProgressPrimitive.Root receives
aria-valuenow/aria-valuemin/aria-valuemax that match the
ProgressPrimitive.Indicator; locate the fragment with ProgressPrimitive.Root and
ProgressPrimitive.Indicator and add the forwarded value prop to Root.

Comment on lines +52 to +54
"react": "^19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.0.0",
Copy link
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

🏁 Script executed:

#!/bin/bash
# Check react-day-picker@8.10.1 peer dependencies
npm view react-day-picker@8.10.1 peerDependencies --json 2>/dev/null | jq .

echo "---"

# Check latest version of react-day-picker
npm view react-day-picker version 2>/dev/null

echo "---"

# Check if there's a React 19 compatible version
npm view react-day-picker versions --json 2>/dev/null | jq '.[-10:]'

Repository: MetaState-Prototype-Project/prototype

Length of output: 297


Upgrade react-day-picker to a React 19-compatible version.

react-day-picker@8.10.1 specifies peer dependencies for React ^16.8.0 || ^17.0.0 || ^18.0.0, which explicitly excludes React 19. Upgrade to version 9.x (e.g., 9.13.0) which supports React 19 to resolve the peer dependency mismatch and prevent runtime issues.

🤖 Prompt for AI Agents
In `@platforms/calendar/package.json` around lines 52 - 54, Update the
react-day-picker dependency in package.json from "react-day-picker": "8.10.1" to
a React 19-compatible 9.x release (for example "react-day-picker": "9.13.0") so
the package's peerDependencies include React 19; modify the dependency string in
package.json and run your package manager install to lock the new version and
resolve the peer dependency mismatch.

@coodos coodos merged commit ea537a2 into main Feb 5, 2026
3 of 4 checks passed
@coodos coodos deleted the feat/calendar branch February 5, 2026 11:26
Copy link
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

🤖 Fix all issues with AI agents
In `@platforms/calendar/components/auth/login-screen.tsx`:
- Around line 65-68: The SSE onerror currently just closes the EventSource
(eventSource.onerror = () => { eventSource.close() }) leaving the user stuck;
update the onerror handler in the login-screen component to surface the failure
and allow retry: set a component state or call an existing error/notification
handler (e.g., setSseError, showToast or dispatch an action) with a clear
message, close the EventSource, and enable a retry path (e.g., set a flag that
shows a "Retry" button or trigger a reconnect with backoff). Ensure the cleanup
return still closes eventSource but that the onerror handler updates UI state so
the user can see the failure and retry the session stream rather than silently
failing.

In `@platforms/calendar/lib/calendar-api.ts`:
- Around line 95-107: The id is interpolated directly into the request URL in
updateEvent and deleteEvent which can break or hijack routes for ids with
special characters; fix both methods (updateEvent and deleteEvent) to encode the
id using encodeURIComponent before constructing the path passed to request
(e.g., build the path as `/api/events/${encodeURIComponent(id)}`) so the URL is
safe and cannot be malformed by special characters.
🧹 Nitpick comments (11)
platforms/calendar/components/calendar/body/current-time-line.tsx (2)

7-18: Time badge and line position can drift out of sync.

The now state triggers re-renders every minute to update the time badge, but getCurrentTimeOffsetTop() is called during render without being tied to state. While this currently works because the state update triggers a re-render which recalculates top, the pattern is fragile—if React batches or skips renders, the line position could lag behind the displayed time.

Consider computing top from the now state directly to ensure they stay synchronized:

♻️ Suggested refactor to derive top from state
+const PIXELS_PER_HOUR = 128 // or import from shared constants
+
 export function CurrentTimeLine() {
   const [now, setNow] = useState(new Date())
   
   useEffect(() => {
     const interval = setInterval(() => {
       setNow(new Date())
     }, 60000) // Update every minute
     return () => clearInterval(interval)
   }, [])
   
-  const top = getCurrentTimeOffsetTop()
+  const hours = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600
+  const top = hours * PIXELS_PER_HOUR

32-32: Re-exporting from a different module creates a confusing import path.

getCurrentTimeOffsetTop is defined in calendar-body-margin-day-margin.tsx but re-exported here. Consumers importing from current-time-line.tsx may be confused about the source of this utility. Consider either importing directly from the source module in consuming files or consolidating time-related utilities into a shared module.

platforms/calendar/components/calendar/body/day/calendar-body-day.tsx (3)

16-18: Unused import: GRID_HEADER_HEIGHT.

GRID_HEADER_HEIGHT is imported but not used in this file. Remove the unused import to keep the code clean.

🧹 Remove unused import
 import {
-  GRID_HEADER_HEIGHT,
   PIXELS_PER_HOUR,
 } from './calendar-body-margin-day-margin'

41-47: Multiple setTimeout calls are a fragile DOM-readiness pattern.

Using three hardcoded timeouts (0, 50, 150ms) to wait for DOM readiness is brittle and may still fail on slower devices or succeed wastefully on faster ones. Consider using requestAnimationFrame or a ResizeObserver for more reliable timing.

♻️ Alternative using requestAnimationFrame
-    // Try multiple times to ensure DOM is ready
-    const timeouts: NodeJS.Timeout[] = []
-    timeouts.push(setTimeout(scrollToCenter, 0))
-    timeouts.push(setTimeout(scrollToCenter, 50))
-    timeouts.push(setTimeout(scrollToCenter, 150))
-    
-    return () => timeouts.forEach(clearTimeout)
+    // Use rAF to ensure layout is complete
+    let rafId: number
+    const attemptScroll = () => {
+      scrollToCenter()
+      // Retry once more after paint if needed
+      rafId = requestAnimationFrame(scrollToCenter)
+    }
+    rafId = requestAnimationFrame(attemptScroll)
+    
+    return () => cancelAnimationFrame(rafId)

20-20: Duplicated constant TOTAL_GRID_HEIGHT across day and week views.

This constant is defined identically in both calendar-body-day.tsx and calendar-body-week.tsx. Consider exporting it from calendar-body-margin-day-margin.tsx alongside PIXELS_PER_HOUR to ensure consistency and reduce duplication.

platforms/calendar/components/calendar/body/week/calendar-body-week.tsx (2)

14-16: Unused import: GRID_HEADER_HEIGHT.

Same as in the day view—GRID_HEADER_HEIGHT is imported but not used.

🧹 Remove unused import
 import {
-  GRID_HEADER_HEIGHT,
   PIXELS_PER_HOUR,
 } from '../day/calendar-body-margin-day-margin'

29-50: Duplicated scroll-to-center logic between day and week views.

The useEffect scroll logic (lines 29-50) is nearly identical to calendar-body-day.tsx. Extract this into a reusable custom hook (e.g., useScrollToCurrentTime) to reduce duplication and ensure consistent behavior across views.

♻️ Extract to custom hook
// hooks/use-scroll-to-current-time.ts
export function useScrollToCurrentTime(
  scrollRef: React.RefObject<HTMLDivElement>,
  enabled: boolean,
  deps: React.DependencyList
) {
  useEffect(() => {
    if (!scrollRef.current || !enabled) return
    const el = scrollRef.current
    const scrollToCenter = () => {
      if (!el.clientHeight || !el.scrollHeight) return
      const top = getCurrentTimeOffsetTop()
      const center = el.clientHeight / 2
      const scrollTop = Math.max(
        0,
        Math.min(top - center, el.scrollHeight - el.clientHeight)
      )
      el.scrollTop = scrollTop
    }
    
    let rafId = requestAnimationFrame(() => {
      scrollToCenter()
      rafId = requestAnimationFrame(scrollToCenter)
    })
    
    return () => cancelAnimationFrame(rafId)
  }, deps)
}
platforms/calendar/next.config.ts (1)

5-15: Consider ESM compatibility for __dirname.

__dirname is not defined in ES modules. While next.config.ts typically runs in a CommonJS context, if the project switches to ESM (e.g., "type": "module" in package.json), this will break. The relevant code snippet from platforms/calendar/eslint.config.mjs shows the ESM-safe pattern being used elsewhere.

Regarding the static analysis ReDoS warning on line 10: this is a false positive since name is only called with a hardcoded string ("NEXT_PUBLIC_CALENDAR_API_URL"), not user input.

🛠️ ESM-safe alternative (optional)
 import type { NextConfig } from "next";
 import path from "path";
 import { readFileSync, existsSync } from "fs";
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
 
 // Read NEXT_PUBLIC_CALENDAR_API_URL from monorepo root .env ...
platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx (2)

58-59: Avoid as any type assertion.

The as any cast on zodResolver(formSchemaBase as any) suggests a type mismatch between zod and react-hook-form types. This can hide type errors.

🛠️ Proposed fix
   const form = useForm<FormValues>({
-    resolver: zodResolver(formSchemaBase as any),
+    resolver: zodResolver(formSchemaBase),
     defaultValues: {

If there's a genuine type error, ensure @hookform/resolvers, react-hook-form, and zod versions are compatible. With the versions listed (3.9.1, 7.54.1, 3.24.1), the cast should be unnecessary.


97-104: Consider static import for calendarApi.

Dynamic importing calendarApi in both onSubmit and handleDelete adds unnecessary overhead since this module will be used whenever the user interacts with events. A static import at the top of the file would be simpler.

♻️ Proposed change

Add to imports at top:

import { calendarApi } from '@/lib/calendar-api'

Then simplify handlers:

-      const { calendarApi } = await import('@/lib/calendar-api')
       await calendarApi.updateEvent(selectedEvent.id, {
platforms/calendar/lib/calendar-api.ts (1)

57-59: Type safety concern with undefined as T for 204 responses.

Casting undefined as T silences TypeScript but can cause runtime issues if a non-void endpoint unexpectedly returns 204. The caller would receive undefined while expecting a typed object.

🛠️ Safer alternative
-  if (res.status === 204) return undefined as T;
+  if (res.status === 204) return undefined as unknown as T;

Or create a separate requestVoid helper for delete operations that explicitly returns Promise<void>.

Comment on lines +65 to +68
eventSource.onerror = () => {
eventSource.close()
}
return () => eventSource.close()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent SSE failure leaves user stuck.

When the EventSource errors, the connection closes without user feedback. If the session stream fails (e.g., network issue, server restart), the user sees no error and remains on the "Scan with your eID Wallet" screen indefinitely with no way to retry.

🛠️ Proposed fix to surface SSE errors
     eventSource.onerror = () => {
       eventSource.close()
+      setError('Connection lost. Please retry.')
     }
📝 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
eventSource.onerror = () => {
eventSource.close()
}
return () => eventSource.close()
eventSource.onerror = () => {
eventSource.close()
setError('Connection lost. Please retry.')
}
return () => eventSource.close()
🤖 Prompt for AI Agents
In `@platforms/calendar/components/auth/login-screen.tsx` around lines 65 - 68,
The SSE onerror currently just closes the EventSource (eventSource.onerror = ()
=> { eventSource.close() }) leaving the user stuck; update the onerror handler
in the login-screen component to surface the failure and allow retry: set a
component state or call an existing error/notification handler (e.g.,
setSseError, showToast or dispatch an action) with a clear message, close the
EventSource, and enable a retry path (e.g., set a flag that shows a "Retry"
button or trigger a reconnect with backoff). Ensure the cleanup return still
closes eventSource but that the onerror handler updates UI state so the user can
see the failure and retry the session stream rather than silently failing.

Comment on lines +95 to +107
updateEvent(
id: string,
body: Partial<{ title: string; color: string; start: string; end: string }>
): Promise<CalendarEventApi> {
return request(`/api/events/${id}`, {
method: "PATCH",
body: JSON.stringify(body),
});
},

deleteEvent(id: string): Promise<void> {
return request(`/api/events/${id}`, { method: "DELETE" });
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Encode id in URL paths to prevent injection issues.

The id parameter is interpolated directly into the URL path without encoding. If id contains special characters (e.g., /, %, ?), it could cause malformed URLs or unintended routing behavior.

🛠️ Proposed fix
   updateEvent(
     id: string,
     body: Partial<{ title: string; color: string; start: string; end: string }>
   ): Promise<CalendarEventApi> {
-    return request(`/api/events/${id}`, {
+    return request(`/api/events/${encodeURIComponent(id)}`, {
       method: "PATCH",
       body: JSON.stringify(body),
     });
   },

   deleteEvent(id: string): Promise<void> {
-    return request(`/api/events/${id}`, { method: "DELETE" });
+    return request(`/api/events/${encodeURIComponent(id)}`, { method: "DELETE" });
   },
📝 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
updateEvent(
id: string,
body: Partial<{ title: string; color: string; start: string; end: string }>
): Promise<CalendarEventApi> {
return request(`/api/events/${id}`, {
method: "PATCH",
body: JSON.stringify(body),
});
},
deleteEvent(id: string): Promise<void> {
return request(`/api/events/${id}`, { method: "DELETE" });
},
updateEvent(
id: string,
body: Partial<{ title: string; color: string; start: string; end: string }>
): Promise<CalendarEventApi> {
return request(`/api/events/${encodeURIComponent(id)}`, {
method: "PATCH",
body: JSON.stringify(body),
});
},
deleteEvent(id: string): Promise<void> {
return request(`/api/events/${encodeURIComponent(id)}`, { method: "DELETE" });
},
🤖 Prompt for AI Agents
In `@platforms/calendar/lib/calendar-api.ts` around lines 95 - 107, The id is
interpolated directly into the request URL in updateEvent and deleteEvent which
can break or hijack routes for ids with special characters; fix both methods
(updateEvent and deleteEvent) to encode the id using encodeURIComponent before
constructing the path passed to request (e.g., build the path as
`/api/events/${encodeURIComponent(id)}`) so the URL is safe and cannot be
malformed by special characters.

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.

4 participants