Add calendar feature with event management and invites#3889
Conversation
Backend: - New `calendar` domain crate (models/ports/service) and `calendar_service` binary, mirroring the contacts service hexagonal layout. - Postgres migration: calendar_event + calendar_attendee tables (instants as epoch-millis BIGINT to match the frontend model and avoid tz ambiguity). - HTTP CRUD for events + an /invite endpoint that records invited attendees; JWT-authenticated, OpenAPI-documented (utoipa). - Repository uses sqlx's runtime-checked API so it compiles without a live DB or prepared offline cache. - Wired into the workspace, docker-compose (port 8101), and service URLs. Frontend plumbing: - @service-calendar client + generated schema types. - @queries/calendar query + mutation hooks. - Codegen config (services.ts, serviceToCrate, orval.config) so the client can be regenerated from the live OpenAPI spec. - Shared calendar domain types, date/ICS/invite utilities. https://claude.ai/code/session_01G5vy6QeqzgpcutwfRqoEje
Format the new crates and add a documented, adapter-scoped allow for the runtime sqlx queries (compile-time macros need a prepared .sqlx cache from a live DB, which isn't available offline). https://claude.ai/code/session_01G5vy6QeqzgpcutwfRqoEje
- block-calendar package: week / day / list views (Google-Calendar-style), click-to-create, event editor dialog with guests, color, and "Save & send invites" (records on the backend + emails attendees from the user's mailbox, with a downloadable .ics). - Vim-style keyboard shortcuts scoped to the calendar: j/k = next/prev screen, t = today, n = new event, w/d/l = week/day/list. `g d` opens it from anywhere. - Sidebar "Calendar" item + a registered `calendar` component view. - CalendarContext wires view/anchor state and editor flow to @queries/calendar. https://claude.ai/code/session_01G5vy6QeqzgpcutwfRqoEje
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis pull request introduces a full-featured calendar system across the entire application stack. The frontend provides a Google Calendar-style interface with week, day, and list views, supporting event creation, editing, guest invitations, and iCalendar export. The backend is a new Rust service implementing RESTful CRUD operations for events and attendees backed by PostgreSQL. Integration points include TypeScript import aliases, hotkey bindings, sidebar navigation, Docker configuration, and database schema migrations. The implementation spans 50+ files with ~2,500 new lines of code. 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
rust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql (1)
39-39: ⚡ Quick winDrop the redundant attendee
event_idindex.On Line 39,
idx_calendar_attendee_eventduplicates the left-most key ofUNIQUE (event_id, email)from Line 36, so it adds write cost without adding new lookup capability for typicalevent_id = ?filters.As per coding guidelines, index strategy should match query patterns, and this index is overlapping with an existing composite unique index.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql` at line 39, Remove the redundant single-column index idx_calendar_attendee_event on table calendar_attendee because it duplicates the left-most key of the existing UNIQUE constraint UNIQUE(event_id, email); update the migration by deleting the CREATE INDEX idx_calendar_attendee_event statement so only the composite unique index remains, ensuring no other migration steps or constraints reference idx_calendar_attendee_event (if they do, remove or adjust those references accordingly).Source: Coding guidelines
js/app/packages/block-calendar/util/dates.ts (1)
37-53: ⚡ Quick winUse
matchfromts-patternfor exhaustive view branching.These branches are exhaustive over
CalendarViewMode, but project conventions requirematch(...)instead of nativeswitchin TS files.As per coding guidelines, “For exhaustive switch statements in TypeScript, use
matchfromts-patterninstead of native switch.”Also applies to: 61-70, 124-138
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/app/packages/block-calendar/util/dates.ts` around lines 37 - 53, Replace the native switch in daysForView with ts-pattern's match to make branching exhaustive: import match from 'ts-pattern', call match(view).with('day', () => ...).with('week', () => ...).with('list', () => ...).exhaustive() (or .otherwise(...) if appropriate) and return the same Date[] results; ensure you preserve use of WEEK_OPTS and the same date-fns helpers (startOfDay, eachDayOfInterval, etc.). Apply the same refactor to the other CalendarViewMode branching sites referenced in the review (the functions handling view-based branching around the other ranges) so all exhaustive switches use match and compile with no implicit any.Source: Coding guidelines
js/app/packages/block-calendar/component/colors.ts (1)
14-41: ⚡ Quick winUse semantic color tokens instead of raw Tailwind palette classes.
EVENT_COLOR_CLASSEScurrently hardcodes palette utilities (bg-*-500,border-*-500). Injs/app/**, styling should use semantic tokens so themes/branding can evolve without touching feature code.As per coding guidelines, "Use semantic color tokens, not raw Tailwind color classes, for styling."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/app/packages/block-calendar/component/colors.ts` around lines 14 - 41, EVENT_COLOR_CLASSES in colors.ts currently uses raw Tailwind palette classes (e.g., bg-blue-500, border-blue-500); update the EVENT_COLOR_CLASSES mapping to use semantic design-system tokens instead (replace bg-*-500 / border-*-500 / bg-*-500 dot/swatch with semantic token classes such as bg-event-<color>-surface, border-event-<color>, text-... and bg-event-<color>-solid for dot/swatch) so theming/branding can change centrally; modify the entries for blue, green, purple, orange, red, pink in the EVENT_COLOR_CLASSES object (and any referenced keys) to the agreed semantic token names from our design system.Source: Coding guidelines
js/app/packages/block-calendar/util/invite.ts (1)
12-17: ⚡ Quick winConsider modernizing base64url encoding to avoid deprecated
unescape.The
btoa(unescape(encodeURIComponent(...)))pattern works but uses the deprecatedunescapefunction. Modern browsers supportTextEncoderfor cleaner UTF-8 handling.♻️ Modern base64url encoding using TextEncoder
-function encodeBodyHtml(html: string): string { - return btoa(unescape(encodeURIComponent(html))) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/={1,}$/, ''); +function encodeBodyHtml(html: string): string { + const bytes = new TextEncoder().encode(html); + const binString = String.fromCodePoint(...bytes); + return btoa(binString) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/={1,}$/, ''); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/app/packages/block-calendar/util/invite.ts` around lines 12 - 17, The encodeBodyHtml function uses the deprecated unescape pattern; update encodeBodyHtml to use TextEncoder to produce a UTF-8 Uint8Array from the html string, convert that byte array into a binary string suitable for btoa (e.g., via String.fromCharCode on chunks of the Uint8Array) or use Buffer.from in Node environments, then call btoa on that binary string and keep the existing base64url replacements (.replace(/\+/g,'-').replace(/\//g,'_').replace(/={1,}$/, '')). Ensure the change preserves UTF-8 correctness and works both in browser (TextEncoder) and when running in Node (Buffer).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docker-compose.yml`:
- Around line 104-119: The calendar_service container lacks a startup dependency
on Postgres so it may attempt DB connections before the DB is ready; update the
calendar_service service block to add a depends_on entry referencing the
Postgres service (e.g., depends_on: - postgres or the project's DB service name)
and include the appropriate condition for service_healthy if you use
healthchecks (e.g., condition: service_healthy) to ensure calendar_service
(command /app/out/calendar_service) waits for the DB before starting.
In `@js/app/packages/block-calendar/component/CalendarContext.tsx`:
- Around line 143-153: sendInvites currently builds recipients from the stale
event.attendees which can omit newly-added emails; change sendInvites to use the
post-invite attendee state returned by inviteMutation.mutateAsync (capture its
result, e.g. const result = await inviteMutation.mutateAsync({ id: event.id,
emails })) and derive attendees from result.attendees (falling back to
event.attendees if absent), then map the requested emails to recipient objects
using those updated attendees (and if an email isn’t present in attendees,
include it with a blank or fallback name) before calling sendInviteEmail; update
references to inviteMutation.mutateAsync, event.attendees, sendInviteEmail, and
emails accordingly.
In `@js/app/packages/block-calendar/component/EventDialog.tsx`:
- Around line 203-207: The onChange for the end time in EventDialog currently
sets endMs from fromDatetimeLocalValue without validation, allowing endMs <
startMs; update the handler in EventDialog so after computing newEnd =
fromDatetimeLocalValue(...) you compare it with the current startMs and
clamp/adjust newEnd to be >= startMs (e.g., newEnd = Math.max(newEnd, startMs))
before calling update({ endMs: newEnd }); ensure you read the current startMs
from the same state/props used by update so the validation uses the
authoritative value.
In `@js/app/packages/block-calendar/util/ics.ts`:
- Around line 53-91: The DTSTART/DTEND lines in buildIcs currently always use
toIcsUtc (DATE-TIME), which misrepresents all-day events; detect all-day events
(e.g., event.allDay) and format those values as DATE (YYYYMMDD) instead of
DATE-TIME, add the VALUE=DATE parameter (e.g., `DTSTART;VALUE=DATE:YYYYMMDD`)
and use a new helper (e.g., toIcsDate) or extend toIcsUtc with a date-only mode
to produce the YYYYMMDD string (no 'T' or 'Z'), and update the code that builds
`DTSTART:${...}` and `DTEND:${...}` in buildIcs to use the DATE formatter when
event.allDay is true. Ensure DTSTAMP and other timed fields remain unchanged.
In `@rust/cloud-storage/calendar_service/src/config.rs`:
- Around line 5-19: The PORT value is parsed/stored as usize which allows values
outside the valid TCP port range; update the Config struct's port field to u16
and change Config::from_env to parse PORT into a u16 (failing with a clear error
via .context on parse) so invalid port numbers are rejected early; ensure any
uses of Config.port expect u16 after this change.
In `@rust/cloud-storage/calendar_service/src/main.rs`:
- Around line 65-67: The code currently uses
tokio::net::TcpListener::bind(...).await.unwrap() (creating variable listener)
which panics on bind failure; change main to return a Result (e.g., Result<(),
Box<dyn std::error::Error>> or anyhow::Result) and replace unwrap() with proper
error propagation or handling: call
tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await.map_err(|e| {
/* wrap with contextual message including port */ })? or match the Result, log a
contextual startup error (including port and e) and return Err; ensure
subsequent uses of listener still compile with the propagated Result from main.
In `@rust/cloud-storage/calendar/src/domain/service.rs`:
- Around line 30-83: The tracing spans currently record PII (user_id, event_id,
attendee emails); update each function's #[instrument(...)] to explicitly skip
those variables: for list_events skip(user_id), for get_event skip(user_id,
event_id), for create_event skip(user_id, request, and ensure user_id isn't
recorded), for update_event skip(user_id, event_id, request), for delete_event
skip(user_id, event_id), and for mark_invited skip(user_id, event_id, emails) so
the instrument macro on the service methods list_events, get_event,
create_event, update_event, delete_event, and mark_invited no longer captures
those sensitive arguments.
In `@rust/cloud-storage/calendar/src/outbound/repository.rs`:
- Around line 209-236: The create_event flow (and similarly update_event and
mark_invited) performs multiple dependent DB writes (INSERT then
upsert_attendees and attendees_for reads) without a transaction; wrap the
multi-step logic in a single sqlx transaction so either all writes succeed or
none are persisted: begin a Transaction via sqlx::PgPool::begin or
pool.begin().await, run the INSERT (currently in create_event) against the
transaction, call upsert_attendees and any attendee reads/updates using the same
transaction (either add transactional variants that accept &mut Transaction or
inline the SQL against the tx), and commit the transaction at the end; on any
error roll back/return without committing. Update update_event (UPDATE + delete
+ upsert) and mark_invited (ownership check + invite upserts) to use the same
transactional pattern, ensuring functions like upsert_attendees and
attendees_for are adapted to accept and use the transaction parameter (or
duplicated transactional SQL) so all steps are atomic.
In
`@rust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql`:
- Around line 8-37: Add DB-level constraints to enforce the documented
invariants: add a CHECK on calendar_event (e.g., constraint name like
chk_calendar_event_start_le_end) to ensure start_ms <= end_ms; add a constrained
type or CHECK on calendar_attendee.status (e.g., create type
calendar_attendee_status or constraint chk_calendar_attendee_status) to restrict
values to the allowed set ('pending','accepted','declined','tentative'); and add
a CHECK on calendar_attendee to enforce invited_ms is NULL while status =
'pending' (or equivalently that invited_ms IS NOT NULL implies status <>
'pending'). Use the existing table/column names calendar_event.start_ms,
calendar_event.end_ms, calendar_attendee.status and calendar_attendee.invited_ms
when adding the constraints so they survive refactors.
---
Nitpick comments:
In `@js/app/packages/block-calendar/component/colors.ts`:
- Around line 14-41: EVENT_COLOR_CLASSES in colors.ts currently uses raw
Tailwind palette classes (e.g., bg-blue-500, border-blue-500); update the
EVENT_COLOR_CLASSES mapping to use semantic design-system tokens instead
(replace bg-*-500 / border-*-500 / bg-*-500 dot/swatch with semantic token
classes such as bg-event-<color>-surface, border-event-<color>, text-... and
bg-event-<color>-solid for dot/swatch) so theming/branding can change centrally;
modify the entries for blue, green, purple, orange, red, pink in the
EVENT_COLOR_CLASSES object (and any referenced keys) to the agreed semantic
token names from our design system.
In `@js/app/packages/block-calendar/util/dates.ts`:
- Around line 37-53: Replace the native switch in daysForView with ts-pattern's
match to make branching exhaustive: import match from 'ts-pattern', call
match(view).with('day', () => ...).with('week', () => ...).with('list', () =>
...).exhaustive() (or .otherwise(...) if appropriate) and return the same Date[]
results; ensure you preserve use of WEEK_OPTS and the same date-fns helpers
(startOfDay, eachDayOfInterval, etc.). Apply the same refactor to the other
CalendarViewMode branching sites referenced in the review (the functions
handling view-based branching around the other ranges) so all exhaustive
switches use match and compile with no implicit any.
In `@js/app/packages/block-calendar/util/invite.ts`:
- Around line 12-17: The encodeBodyHtml function uses the deprecated unescape
pattern; update encodeBodyHtml to use TextEncoder to produce a UTF-8 Uint8Array
from the html string, convert that byte array into a binary string suitable for
btoa (e.g., via String.fromCharCode on chunks of the Uint8Array) or use
Buffer.from in Node environments, then call btoa on that binary string and keep
the existing base64url replacements
(.replace(/\+/g,'-').replace(/\//g,'_').replace(/={1,}$/, '')). Ensure the
change preserves UTF-8 correctness and works both in browser (TextEncoder) and
when running in Node (Buffer).
In
`@rust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql`:
- Line 39: Remove the redundant single-column index idx_calendar_attendee_event
on table calendar_attendee because it duplicates the left-most key of the
existing UNIQUE constraint UNIQUE(event_id, email); update the migration by
deleting the CREATE INDEX idx_calendar_attendee_event statement so only the
composite unique index remains, ensuring no other migration steps or constraints
reference idx_calendar_attendee_event (if they do, remove or adjust those
references accordingly).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 60cd470b-6bd7-41e6-8974-b1191ce7bfb3
⛔ Files ignored due to path filters (8)
js/app/packages/service-clients/service-calendar/generated/schemas/attendee.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/attendeeInput.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/calendarEvent.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/createEventRequest.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/index.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/inviteRequest.tsis excluded by!**/generated/**js/app/packages/service-clients/service-calendar/generated/schemas/updateEventRequest.tsis excluded by!**/generated/**rust/cloud-storage/Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (45)
docker-compose.ymljs/app/packages/app/component/app-sidebar/sidebar.tsxjs/app/packages/app/component/split-layout/componentRegistry.tsxjs/app/packages/block-calendar/README.mdjs/app/packages/block-calendar/component/Calendar.tsxjs/app/packages/block-calendar/component/CalendarContext.tsxjs/app/packages/block-calendar/component/EventDialog.tsxjs/app/packages/block-calendar/component/ListView.tsxjs/app/packages/block-calendar/component/TimeGrid.tsxjs/app/packages/block-calendar/component/Toolbar.tsxjs/app/packages/block-calendar/component/colors.tsjs/app/packages/block-calendar/index.tsjs/app/packages/block-calendar/model/types.tsjs/app/packages/block-calendar/package.jsonjs/app/packages/block-calendar/tsconfig.jsonjs/app/packages/block-calendar/util/dates.tsjs/app/packages/block-calendar/util/ics.tsjs/app/packages/block-calendar/util/invite.tsjs/app/packages/core/constant/servers.tsjs/app/packages/core/hotkey/tokens.tsjs/app/packages/icon/wide-calendar.tsxjs/app/packages/queries/calendar/events.tsjs/app/packages/queries/calendar/keys.tsjs/app/packages/service-clients/orval.config.tsjs/app/packages/service-clients/service-calendar/client.tsjs/app/scripts/generate-api-schema.tsjs/app/scripts/services.tsjs/app/tsconfig.jsonrust/cloud-storage/Cargo.tomlrust/cloud-storage/calendar/Cargo.tomlrust/cloud-storage/calendar/src/domain.rsrust/cloud-storage/calendar/src/domain/models.rsrust/cloud-storage/calendar/src/domain/ports.rsrust/cloud-storage/calendar/src/domain/service.rsrust/cloud-storage/calendar/src/inbound.rsrust/cloud-storage/calendar/src/inbound/http.rsrust/cloud-storage/calendar/src/lib.rsrust/cloud-storage/calendar/src/outbound.rsrust/cloud-storage/calendar/src/outbound/repository.rsrust/cloud-storage/calendar_service/Cargo.tomlrust/cloud-storage/calendar_service/src/config.rsrust/cloud-storage/calendar_service/src/health.rsrust/cloud-storage/calendar_service/src/main.rsrust/cloud-storage/calendar_service/src/openapi.rsrust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql
| calendar_service: | ||
| <<: [*common-env, *rust-services-image] | ||
| command: ["/app/out/calendar_service"] | ||
| ports: | ||
| - "8101:8080" | ||
| networks: | ||
| databases: | ||
| services: | ||
| aliases: | ||
| - calendar-service | ||
| healthcheck: | ||
| test: ["CMD", "curl", "-f", "http://localhost:8080/health"] | ||
| interval: 30s | ||
| timeout: 10s | ||
| retries: 3 | ||
| start_period: 10s |
There was a problem hiding this comment.
Add Postgres startup dependency for local reliability.
calendar_service is DB-backed but has no depends_on, so it can start before Postgres and fail during initial connect. Add a Postgres dependency to match existing service startup patterns.
Suggested fix
calendar_service:
<<: [*common-env, *rust-services-image]
command: ["/app/out/calendar_service"]
ports:
- "8101:8080"
+ depends_on:
+ - postgres
networks:
databases:
services:
aliases:
- calendar-service📝 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.
| calendar_service: | |
| <<: [*common-env, *rust-services-image] | |
| command: ["/app/out/calendar_service"] | |
| ports: | |
| - "8101:8080" | |
| networks: | |
| databases: | |
| services: | |
| aliases: | |
| - calendar-service | |
| healthcheck: | |
| test: ["CMD", "curl", "-f", "http://localhost:8080/health"] | |
| interval: 30s | |
| timeout: 10s | |
| retries: 3 | |
| start_period: 10s | |
| calendar_service: | |
| <<: [*common-env, *rust-services-image] | |
| command: ["/app/out/calendar_service"] | |
| ports: | |
| - "8101:8080" | |
| depends_on: | |
| - postgres | |
| networks: | |
| databases: | |
| services: | |
| aliases: | |
| - calendar-service | |
| healthcheck: | |
| test: ["CMD", "curl", "-f", "http://localhost:8080/health"] | |
| interval: 30s | |
| timeout: 10s | |
| retries: 3 | |
| start_period: 10s |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docker-compose.yml` around lines 104 - 119, The calendar_service container
lacks a startup dependency on Postgres so it may attempt DB connections before
the DB is ready; update the calendar_service service block to add a depends_on
entry referencing the Postgres service (e.g., depends_on: - postgres or the
project's DB service name) and include the appropriate condition for
service_healthy if you use healthchecks (e.g., condition: service_healthy) to
ensure calendar_service (command /app/out/calendar_service) waits for the DB
before starting.
| const sendInvites = async (event: CalendarEvent, emails: string[]) => { | ||
| await inviteMutation.mutateAsync({ id: event.id, emails }); | ||
| const recipients = event.attendees | ||
| .filter((a) => emails.includes(a.email)) | ||
| .map((a) => ({ email: a.email, name: a.name })); | ||
| return sendInviteEmail({ | ||
| event, | ||
| organizerEmail: user.email() ?? '', | ||
| organizerName: user.author(), | ||
| recipients: recipients.length > 0 ? recipients : undefined, | ||
| }); |
There was a problem hiding this comment.
Use the post-invite attendee state (or requested emails) when building recipients.
Line 145 currently filters recipients from the stale event.attendees input. If emails contains newly-added attendees, they may be omitted from sendInviteEmail, so backend invite state and outbound email delivery diverge.
Suggested fix
const sendInvites = async (event: CalendarEvent, emails: string[]) => {
- await inviteMutation.mutateAsync({ id: event.id, emails });
- const recipients = event.attendees
- .filter((a) => emails.includes(a.email))
- .map((a) => ({ email: a.email, name: a.name }));
+ const invitedWire = await inviteMutation.mutateAsync({ id: event.id, emails });
+ const invitedEvent = toDomain(invitedWire);
+ const recipients = emails.map((email) => {
+ const attendee = invitedEvent.attendees.find((a) => a.email === email);
+ return { email, name: attendee?.name };
+ });
return sendInviteEmail({
- event,
+ event: invitedEvent,
organizerEmail: user.email() ?? '',
organizerName: user.author(),
- recipients: recipients.length > 0 ? recipients : undefined,
+ recipients,
});
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/app/packages/block-calendar/component/CalendarContext.tsx` around lines
143 - 153, sendInvites currently builds recipients from the stale
event.attendees which can omit newly-added emails; change sendInvites to use the
post-invite attendee state returned by inviteMutation.mutateAsync (capture its
result, e.g. const result = await inviteMutation.mutateAsync({ id: event.id,
emails })) and derive attendees from result.attendees (falling back to
event.attendees if absent), then map the requested emails to recipient objects
using those updated attendees (and if an email isn’t present in attendees,
include it with a blank or fallback name) before calling sendInviteEmail; update
references to inviteMutation.mutateAsync, event.attendees, sendInviteEmail, and
emails accordingly.
| onChange={(e) => | ||
| update({ | ||
| endMs: fromDatetimeLocalValue(e.currentTarget.value), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Prevent invalid time ranges when editing the end time.
Line 203-206 allows endMs to become earlier than startMs. This can persist invalid event intervals and break downstream rendering logic that assumes non-negative duration.
Suggested fix
- onChange={(e) =>
- update({
- endMs: fromDatetimeLocalValue(e.currentTarget.value),
- })
- }
+ onChange={(e) => {
+ const parsedEnd = fromDatetimeLocalValue(
+ e.currentTarget.value
+ );
+ const start = currentDraft().startMs;
+ if (Number.isNaN(parsedEnd)) return;
+ update({ endMs: Math.max(parsedEnd, start) });
+ }}📝 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.
| onChange={(e) => | |
| update({ | |
| endMs: fromDatetimeLocalValue(e.currentTarget.value), | |
| }) | |
| } | |
| onChange={(e) => { | |
| const parsedEnd = fromDatetimeLocalValue( | |
| e.currentTarget.value | |
| ); | |
| const start = currentDraft().startMs; | |
| if (Number.isNaN(parsedEnd)) return; | |
| update({ endMs: Math.max(parsedEnd, start) }); | |
| }} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/app/packages/block-calendar/component/EventDialog.tsx` around lines 203 -
207, The onChange for the end time in EventDialog currently sets endMs from
fromDatetimeLocalValue without validation, allowing endMs < startMs; update the
handler in EventDialog so after computing newEnd = fromDatetimeLocalValue(...)
you compare it with the current startMs and clamp/adjust newEnd to be >= startMs
(e.g., newEnd = Math.max(newEnd, startMs)) before calling update({ endMs: newEnd
}); ensure you read the current startMs from the same state/props used by update
so the validation uses the authoritative value.
| export function buildIcs(event: CalendarEvent, opts: IcsOptions): string { | ||
| const method = opts.method ?? 'REQUEST'; | ||
| const lines: string[] = [ | ||
| 'BEGIN:VCALENDAR', | ||
| 'VERSION:2.0', | ||
| 'PRODID:-//Macro//Calendar//EN', | ||
| 'CALSCALE:GREGORIAN', | ||
| `METHOD:${method}`, | ||
| 'BEGIN:VEVENT', | ||
| `UID:${event.id}@macro.com`, | ||
| `DTSTAMP:${toIcsUtc(Date.now())}`, | ||
| `DTSTART:${toIcsUtc(event.startMs)}`, | ||
| `DTEND:${toIcsUtc(event.endMs)}`, | ||
| `SUMMARY:${escapeText(event.title)}`, | ||
| ]; | ||
|
|
||
| if (event.description) { | ||
| lines.push(`DESCRIPTION:${escapeText(event.description)}`); | ||
| } | ||
| if (event.location) { | ||
| lines.push(`LOCATION:${escapeText(event.location)}`); | ||
| } | ||
|
|
||
| const organizerCn = opts.organizerName ?? opts.organizerEmail; | ||
| lines.push( | ||
| `ORGANIZER;CN=${escapeText(organizerCn)}:mailto:${opts.organizerEmail}` | ||
| ); | ||
|
|
||
| for (const attendee of event.attendees) { | ||
| const cn = escapeText(attendee.name ?? attendee.email); | ||
| lines.push( | ||
| `ATTENDEE;CN=${cn};ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:${attendee.email}` | ||
| ); | ||
| } | ||
|
|
||
| lines.push('STATUS:CONFIRMED', 'END:VEVENT', 'END:VCALENDAR'); | ||
|
|
||
| return lines.map(foldLine).join('\r\n'); | ||
| } |
There was a problem hiding this comment.
All-day events must use DATE format, not DATETIME.
RFC-5545 §3.3.4 requires all-day events to use the DATE value type (YYYYMMDD) for DTSTART and DTEND, not the DATE-TIME format (YYYYMMDDTHHMMSSZ) that toIcsUtc produces. Calendar clients will misinterpret all-day events as timed events starting at midnight UTC.
🗓️ Proposed fix to handle all-day events correctly
Add a DATE formatter and use it conditionally:
}
+/** Format an epoch-ms instant as a DATE value (e.g. 20260612). */
+function toIcsDate(ms: number): string {
+ const d = new Date(ms);
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
+}
+
/** Escape per RFC-5545 §3.3.11 (commas, semicolons, backslashes, newlines). */ const lines: string[] = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Macro//Calendar//EN',
'CALSCALE:GREGORIAN',
`METHOD:${method}`,
'BEGIN:VEVENT',
`UID:${event.id}`@macro.com``,
`DTSTAMP:${toIcsUtc(Date.now())}`,
- `DTSTART:${toIcsUtc(event.startMs)}`,
- `DTEND:${toIcsUtc(event.endMs)}`,
+ event.allDay
+ ? `DTSTART;VALUE=DATE:${toIcsDate(event.startMs)}`
+ : `DTSTART:${toIcsUtc(event.startMs)}`,
+ event.allDay
+ ? `DTEND;VALUE=DATE:${toIcsDate(event.endMs)}`
+ : `DTEND:${toIcsUtc(event.endMs)}`,
`SUMMARY:${escapeText(event.title)}`,
];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/app/packages/block-calendar/util/ics.ts` around lines 53 - 91, The
DTSTART/DTEND lines in buildIcs currently always use toIcsUtc (DATE-TIME), which
misrepresents all-day events; detect all-day events (e.g., event.allDay) and
format those values as DATE (YYYYMMDD) instead of DATE-TIME, add the VALUE=DATE
parameter (e.g., `DTSTART;VALUE=DATE:YYYYMMDD`) and use a new helper (e.g.,
toIcsDate) or extend toIcsUtc with a date-only mode to produce the YYYYMMDD
string (no 'T' or 'Z'), and update the code that builds `DTSTART:${...}` and
`DTEND:${...}` in buildIcs to use the DATE formatter when event.allDay is true.
Ensure DTSTAMP and other timed fields remain unchanged.
| pub struct Config { | ||
| /// Port number to listen on. | ||
| pub port: usize, | ||
| /// The deployment environment. | ||
| pub environment: Environment, | ||
| /// Postgres connection URL. | ||
| pub database_url: String, | ||
| } | ||
|
|
||
| impl Config { | ||
| pub fn from_env() -> anyhow::Result<Self> { | ||
| let port: usize = std::env::var("PORT") | ||
| .unwrap_or("8080".to_string()) | ||
| .parse::<usize>() | ||
| .context("PORT must be a valid number")?; |
There was a problem hiding this comment.
Constrain PORT to a valid TCP port range.
usize accepts values outside 0..=65535, so invalid ports are accepted in config and fail later during bind. Parse/store as u16 to fail fast with a clear config error.
Suggested fix
pub struct Config {
/// Port number to listen on.
- pub port: usize,
+ pub port: u16,
@@
- let port: usize = std::env::var("PORT")
+ let port: u16 = std::env::var("PORT")
.unwrap_or("8080".to_string())
- .parse::<usize>()
- .context("PORT must be a valid number")?;
+ .parse::<u16>()
+ .context("PORT must be a valid TCP port (0-65535)")?;📝 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.
| pub struct Config { | |
| /// Port number to listen on. | |
| pub port: usize, | |
| /// The deployment environment. | |
| pub environment: Environment, | |
| /// Postgres connection URL. | |
| pub database_url: String, | |
| } | |
| impl Config { | |
| pub fn from_env() -> anyhow::Result<Self> { | |
| let port: usize = std::env::var("PORT") | |
| .unwrap_or("8080".to_string()) | |
| .parse::<usize>() | |
| .context("PORT must be a valid number")?; | |
| pub struct Config { | |
| /// Port number to listen on. | |
| pub port: u16, | |
| /// The deployment environment. | |
| pub environment: Environment, | |
| /// Postgres connection URL. | |
| pub database_url: String, | |
| } | |
| impl Config { | |
| pub fn from_env() -> anyhow::Result<Self> { | |
| let port: u16 = std::env::var("PORT") | |
| .unwrap_or("8080".to_string()) | |
| .parse::<u16>() | |
| .context("PORT must be a valid TCP port (0-65535)")?; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rust/cloud-storage/calendar_service/src/config.rs` around lines 5 - 19, The
PORT value is parsed/stored as usize which allows values outside the valid TCP
port range; update the Config struct's port field to u16 and change
Config::from_env to parse PORT into a u16 (failing with a clear error via
.context on parse) so invalid port numbers are rejected early; ensure any uses
of Config.port expect u16 after this change.
| let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) | ||
| .await | ||
| .unwrap(); |
There was a problem hiding this comment.
Avoid panic on listener bind failure.
Line 67 uses unwrap(), so a transient bind failure crashes the process instead of returning a contextual startup error.
Suggested fix
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
.await
- .unwrap();
+ .context("failed to bind calendar service TCP listener")?;📝 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.
| let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) | |
| .await | |
| .unwrap(); | |
| let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) | |
| .await | |
| .context("failed to bind calendar service TCP listener")?; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rust/cloud-storage/calendar_service/src/main.rs` around lines 65 - 67, The
code currently uses tokio::net::TcpListener::bind(...).await.unwrap() (creating
variable listener) which panics on bind failure; change main to return a Result
(e.g., Result<(), Box<dyn std::error::Error>> or anyhow::Result) and replace
unwrap() with proper error propagation or handling: call
tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await.map_err(|e| {
/* wrap with contextual message including port */ })? or match the Result, log a
contextual startup error (including port and e) and return Err; ensure
subsequent uses of listener still compile with the propagated Result from main.
| #[instrument(err, skip(self))] | ||
| async fn list_events( | ||
| &self, | ||
| user_id: &str, | ||
| start_ms: i64, | ||
| end_ms: i64, | ||
| ) -> Result<Vec<CalendarEvent>, Report> { | ||
| self.repository.list_events(user_id, start_ms, end_ms).await | ||
| } | ||
|
|
||
| #[instrument(err, skip(self))] | ||
| async fn get_event( | ||
| &self, | ||
| user_id: &str, | ||
| event_id: &str, | ||
| ) -> Result<Option<CalendarEvent>, Report> { | ||
| self.repository.get_event(user_id, event_id).await | ||
| } | ||
|
|
||
| #[instrument(err, skip(self, request))] | ||
| async fn create_event( | ||
| &self, | ||
| user_id: &str, | ||
| request: CreateEventRequest, | ||
| ) -> Result<CalendarEvent, Report> { | ||
| self.repository | ||
| .create_event(user_id, normalize(request)) | ||
| .await | ||
| } | ||
|
|
||
| #[instrument(err, skip(self, request))] | ||
| async fn update_event( | ||
| &self, | ||
| user_id: &str, | ||
| event_id: &str, | ||
| request: UpdateEventRequest, | ||
| ) -> Result<Option<CalendarEvent>, Report> { | ||
| self.repository | ||
| .update_event(user_id, event_id, normalize(request)) | ||
| .await | ||
| } | ||
|
|
||
| #[instrument(err, skip(self))] | ||
| async fn delete_event(&self, user_id: &str, event_id: &str) -> Result<bool, Report> { | ||
| self.repository.delete_event(user_id, event_id).await | ||
| } | ||
|
|
||
| #[instrument(err, skip(self))] | ||
| async fn mark_invited( | ||
| &self, | ||
| user_id: &str, | ||
| event_id: &str, | ||
| emails: Vec<String>, | ||
| ) -> Result<Option<CalendarEvent>, Report> { |
There was a problem hiding this comment.
Stop recording user_id/event_id/emails in tracing spans.
On Line 30, Line 40, Line 49, Line 60, Line 72, and Line 77, current #[instrument] usage still captures PII-bearing arguments (notably user_id, and in mark_invited, attendee emails). Please skip these fields explicitly.
🔒 Proposed fix
- #[instrument(err, skip(self))]
+ #[instrument(err, skip(self, user_id))]
@@
- #[instrument(err, skip(self))]
+ #[instrument(err, skip(self, user_id, event_id))]
@@
- #[instrument(err, skip(self, request))]
+ #[instrument(err, skip(self, user_id, request))]
@@
- #[instrument(err, skip(self, request))]
+ #[instrument(err, skip(self, user_id, event_id, request))]
@@
- #[instrument(err, skip(self))]
+ #[instrument(err, skip(self, user_id, event_id))]
@@
- #[instrument(err, skip(self))]
+ #[instrument(err, skip(self, user_id, event_id, emails))]Based on learnings, MacroUserIdStr is in macro|useremail.com form, so logging user_id is logging user-identifying data.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[instrument(err, skip(self))] | |
| async fn list_events( | |
| &self, | |
| user_id: &str, | |
| start_ms: i64, | |
| end_ms: i64, | |
| ) -> Result<Vec<CalendarEvent>, Report> { | |
| self.repository.list_events(user_id, start_ms, end_ms).await | |
| } | |
| #[instrument(err, skip(self))] | |
| async fn get_event( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| ) -> Result<Option<CalendarEvent>, Report> { | |
| self.repository.get_event(user_id, event_id).await | |
| } | |
| #[instrument(err, skip(self, request))] | |
| async fn create_event( | |
| &self, | |
| user_id: &str, | |
| request: CreateEventRequest, | |
| ) -> Result<CalendarEvent, Report> { | |
| self.repository | |
| .create_event(user_id, normalize(request)) | |
| .await | |
| } | |
| #[instrument(err, skip(self, request))] | |
| async fn update_event( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| request: UpdateEventRequest, | |
| ) -> Result<Option<CalendarEvent>, Report> { | |
| self.repository | |
| .update_event(user_id, event_id, normalize(request)) | |
| .await | |
| } | |
| #[instrument(err, skip(self))] | |
| async fn delete_event(&self, user_id: &str, event_id: &str) -> Result<bool, Report> { | |
| self.repository.delete_event(user_id, event_id).await | |
| } | |
| #[instrument(err, skip(self))] | |
| async fn mark_invited( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| emails: Vec<String>, | |
| ) -> Result<Option<CalendarEvent>, Report> { | |
| #[instrument(err, skip(self, user_id))] | |
| async fn list_events( | |
| &self, | |
| user_id: &str, | |
| start_ms: i64, | |
| end_ms: i64, | |
| ) -> Result<Vec<CalendarEvent>, Report> { | |
| self.repository.list_events(user_id, start_ms, end_ms).await | |
| } | |
| #[instrument(err, skip(self, user_id, event_id))] | |
| async fn get_event( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| ) -> Result<Option<CalendarEvent>, Report> { | |
| self.repository.get_event(user_id, event_id).await | |
| } | |
| #[instrument(err, skip(self, user_id, request))] | |
| async fn create_event( | |
| &self, | |
| user_id: &str, | |
| request: CreateEventRequest, | |
| ) -> Result<CalendarEvent, Report> { | |
| self.repository | |
| .create_event(user_id, normalize(request)) | |
| .await | |
| } | |
| #[instrument(err, skip(self, user_id, event_id, request))] | |
| async fn update_event( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| request: UpdateEventRequest, | |
| ) -> Result<Option<CalendarEvent>, Report> { | |
| self.repository | |
| .update_event(user_id, event_id, normalize(request)) | |
| .await | |
| } | |
| #[instrument(err, skip(self, user_id, event_id))] | |
| async fn delete_event(&self, user_id: &str, event_id: &str) -> Result<bool, Report> { | |
| self.repository.delete_event(user_id, event_id).await | |
| } | |
| #[instrument(err, skip(self, user_id, event_id, emails))] | |
| async fn mark_invited( | |
| &self, | |
| user_id: &str, | |
| event_id: &str, | |
| emails: Vec<String>, | |
| ) -> Result<Option<CalendarEvent>, Report> { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rust/cloud-storage/calendar/src/domain/service.rs` around lines 30 - 83, The
tracing spans currently record PII (user_id, event_id, attendee emails); update
each function's #[instrument(...)] to explicitly skip those variables: for
list_events skip(user_id), for get_event skip(user_id, event_id), for
create_event skip(user_id, request, and ensure user_id isn't recorded), for
update_event skip(user_id, event_id, request), for delete_event skip(user_id,
event_id), and for mark_invited skip(user_id, event_id, emails) so the
instrument macro on the service methods list_events, get_event, create_event,
update_event, delete_event, and mark_invited no longer captures those sensitive
arguments.
Source: Learnings
| async fn create_event( | ||
| &self, | ||
| user_id: &str, | ||
| request: CreateEventRequest, | ||
| ) -> Result<CalendarEvent, Report> { | ||
| let row = sqlx::query_as::<_, EventRow>(&format!( | ||
| "INSERT INTO calendar_event | ||
| (user_id, title, description, location, start_ms, end_ms, all_day, color) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8) | ||
| RETURNING {EVENT_COLUMNS}" | ||
| )) | ||
| .bind(user_id) | ||
| .bind(&request.title) | ||
| .bind(&request.description) | ||
| .bind(&request.location) | ||
| .bind(request.start_ms) | ||
| .bind(request.end_ms) | ||
| .bind(request.all_day) | ||
| .bind(&request.color) | ||
| .fetch_one(&self.db) | ||
| .await?; | ||
|
|
||
| self.upsert_attendees(row.id, &request.attendees).await?; | ||
|
|
||
| let mut grouped = self.attendees_for(&[row.id]).await?; | ||
| let attendees = grouped.remove(&row.id).unwrap_or_default(); | ||
| Ok(row.into_event(attendees)) | ||
| } |
There was a problem hiding this comment.
Wrap multi-step event mutations in a DB transaction
These handlers perform dependent writes across multiple statements without a transaction. A mid-operation failure can leave partially-applied state (e.g., event row written/updated but attendee sync incomplete), and mark_invited has a check-then-write race window.
Suggested fix pattern
+let mut tx = self.db.begin().await?;
let row = sqlx::query_as::<_, EventRow>(/* ... */)
- .fetch_one(&self.db)
+ .fetch_one(&mut *tx)
.await?;
-self.upsert_attendees(row.id, &request.attendees).await?;
+self.upsert_attendees_tx(&mut tx, row.id, &request.attendees).await?;
+tx.commit().await?;Apply the same transactional pattern to update_event (update + delete + upsert) and mark_invited (ownership check + invite upserts).
Also applies to: 238-282, 296-326
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rust/cloud-storage/calendar/src/outbound/repository.rs` around lines 209 -
236, The create_event flow (and similarly update_event and mark_invited)
performs multiple dependent DB writes (INSERT then upsert_attendees and
attendees_for reads) without a transaction; wrap the multi-step logic in a
single sqlx transaction so either all writes succeed or none are persisted:
begin a Transaction via sqlx::PgPool::begin or pool.begin().await, run the
INSERT (currently in create_event) against the transaction, call
upsert_attendees and any attendee reads/updates using the same transaction
(either add transactional variants that accept &mut Transaction or inline the
SQL against the tx), and commit the transaction at the end; on any error roll
back/return without committing. Update update_event (UPDATE + delete + upsert)
and mark_invited (ownership check + invite upserts) to use the same
transactional pattern, ensuring functions like upsert_attendees and
attendees_for are adapted to accept and use the transaction parameter (or
duplicated transactional SQL) so all steps are atomic.
| CREATE TABLE calendar_event ( | ||
| id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| user_id text NOT NULL REFERENCES "User"(id) ON UPDATE CASCADE ON DELETE CASCADE, | ||
| title text NOT NULL DEFAULT '', | ||
| description text, | ||
| location text, | ||
| start_ms bigint NOT NULL, | ||
| end_ms bigint NOT NULL, | ||
| all_day boolean NOT NULL DEFAULT false, | ||
| color text NOT NULL DEFAULT 'blue', | ||
| created_ms bigint NOT NULL DEFAULT (floor(extract(epoch FROM now()) * 1000))::bigint, | ||
| updated_ms bigint NOT NULL DEFAULT (floor(extract(epoch FROM now()) * 1000))::bigint | ||
| ); | ||
|
|
||
| -- Range scans for "events for this user between start and end" are the hot path. | ||
| CREATE INDEX idx_calendar_event_user_range | ||
| ON calendar_event (user_id, start_ms, end_ms); | ||
|
|
||
| CREATE TABLE calendar_attendee ( | ||
| id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| event_id uuid NOT NULL REFERENCES calendar_event (id) ON DELETE CASCADE, | ||
| email text NOT NULL, | ||
| name text, | ||
| -- One of: pending | accepted | declined | tentative | ||
| status text NOT NULL DEFAULT 'pending', | ||
| -- Epoch-millis the invite email was sent, NULL until invited. | ||
| invited_ms bigint, | ||
| created_ms bigint NOT NULL DEFAULT (floor(extract(epoch FROM now()) * 1000))::bigint, | ||
| UNIQUE (event_id, email) | ||
| ); |
There was a problem hiding this comment.
Enforce calendar invariants at the database layer.
On Line 8-20 and Line 26-37, critical domain rules are only documented/in code, not enforced in schema. This allows invalid persisted rows (start_ms > end_ms, arbitrary status) if writes bypass/bug the service path.
🧱 Proposed fix
CREATE TABLE calendar_event (
@@
start_ms bigint NOT NULL,
end_ms bigint NOT NULL,
@@
- updated_ms bigint NOT NULL DEFAULT (floor(extract(epoch FROM now()) * 1000))::bigint
+ updated_ms bigint NOT NULL DEFAULT (floor(extract(epoch FROM now()) * 1000))::bigint,
+ CONSTRAINT calendar_event_time_order_chk CHECK (start_ms <= end_ms)
);
@@
CREATE TABLE calendar_attendee (
@@
- status text NOT NULL DEFAULT 'pending',
+ status text NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'accepted', 'declined', 'tentative')),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@rust/cloud-storage/macro_db_client/migrations/20260609120000_calendar_db_schema.sql`
around lines 8 - 37, Add DB-level constraints to enforce the documented
invariants: add a CHECK on calendar_event (e.g., constraint name like
chk_calendar_event_start_le_end) to ensure start_ms <= end_ms; add a constrained
type or CHECK on calendar_attendee.status (e.g., create type
calendar_attendee_status or constraint chk_calendar_attendee_status) to restrict
values to the allowed set ('pending','accepted','declined','tentative'); and add
a CHECK on calendar_attendee to enforce invited_ms is NULL while status =
'pending' (or equivalently that invited_ms IS NOT NULL implies status <>
'pending'). Use the existing table/column names calendar_event.start_ms,
calendar_event.end_ms, calendar_attendee.status and calendar_attendee.invited_ms
when adding the constraints so they survive refactors.
Summary
Introduces a complete calendar feature enabling users to create, edit, and manage events with attendee invitations. Includes a full-stack implementation with a Rust backend service, PostgreSQL persistence, and a Solid.js frontend UI with multiple view modes.
Key Changes
Backend (Rust)
rust/cloud-storage/calendar_service/) - New microservice with HTTP API and health checksrust/cloud-storage/calendar/) - Hexagonal architecture with:calendar_eventandcalendar_attendeetables storing instants as epoch-millisFrontend (TypeScript/Solid.js)
js/app/packages/block-calendar/) - New package with:Integration
Notable Implementation Details
https://claude.ai/code/session_01G5vy6QeqzgpcutwfRqoEje