Files
stack/apps/web/src/components/federation/FederatedEventCard.tsx
Jason Woltje 8178617e53 feat(#92): implement Aggregated Dashboard View
Implement unified dashboard to display tasks and events from multiple
federated Mosaic Stack instances with clear provenance indicators.

Backend Integration:
- Extended federation API client with query support (sendFederatedQuery)
- Added query message fetching functions
- Integrated with existing QUERY message type from Phase 3

Components Created:
- ProvenanceIndicator: Shows which instance data came from
- FederatedTaskCard: Task display with provenance
- FederatedEventCard: Event display with provenance
- AggregatedDataGrid: Unified grid for multiple data types
- Dashboard page at /federation/dashboard

Key Features:
- Query all ACTIVE federated connections on load
- Display aggregated tasks and events in unified view
- Clear provenance indicators (instance name badges)
- PDA-friendly language throughout (no demanding terms)
- Loading states and error handling
- Empty state when no connections available

Technical Implementation:
- Uses POST /api/v1/federation/query to send queries
- Queries each connection for tasks.list and events.list
- Aggregates responses with provenance metadata
- Handles connection failures gracefully
- 86 tests passing with >85% coverage
- TypeScript strict mode compliant
- ESLint compliant

PDA-Friendly Design:
- "Unable to reach" instead of "Connection failed"
- "No data available" instead of "No results"
- "Loading data from instances..." instead of "Fetching..."
- Calm color palette (soft blues, greens, grays)
- Status indicators: 🟢 Active, 📋 No data, ⚠️ Error

Files Added:
- apps/web/src/lib/api/federation-queries.ts
- apps/web/src/lib/api/federation-queries.test.ts
- apps/web/src/components/federation/types.ts
- apps/web/src/components/federation/ProvenanceIndicator.tsx
- apps/web/src/components/federation/ProvenanceIndicator.test.tsx
- apps/web/src/components/federation/FederatedTaskCard.tsx
- apps/web/src/components/federation/FederatedTaskCard.test.tsx
- apps/web/src/components/federation/FederatedEventCard.tsx
- apps/web/src/components/federation/FederatedEventCard.test.tsx
- apps/web/src/components/federation/AggregatedDataGrid.tsx
- apps/web/src/components/federation/AggregatedDataGrid.test.tsx
- apps/web/src/app/(authenticated)/federation/dashboard/page.tsx
- docs/scratchpads/92-aggregated-dashboard.md

Testing:
- 86 total tests passing
- Unit tests for all components
- Integration tests for API client
- PDA-friendly language verified
- TypeScript type checking passing
- ESLint passing

Ready for code review and QA testing.

Related Issues:
- Depends on #85 (FED-005: QUERY Message Type) - COMPLETED
- Depends on #91 (FED-008: Connection Manager UI) - COMPLETED
- Uses #90 (FED-007: EVENT Subscriptions) infrastructure

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 14:18:18 -06:00

95 lines
2.6 KiB
TypeScript

/**
* FederatedEventCard Component
* Displays an event from a federated instance with provenance indicator
*/
import type { FederatedEvent } from "./types";
import { ProvenanceIndicator } from "./ProvenanceIndicator";
interface FederatedEventCardProps {
federatedEvent: FederatedEvent;
compact?: boolean;
onClick?: () => void;
}
/**
* Format time for display
*/
function formatTime(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
}).format(new Date(date));
}
/**
* Format date for display
*/
function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
weekday: "short",
month: "short",
day: "numeric",
}).format(new Date(date));
}
export function FederatedEventCard({
federatedEvent,
compact = false,
onClick,
}: FederatedEventCardProps): React.JSX.Element {
const { event, provenance } = federatedEvent;
const paddingClass = compact ? "p-3" : "p-4";
const clickableClass = onClick ? "cursor-pointer hover:border-gray-300" : "";
const startTime = formatTime(event.startTime);
const endTime = event.endTime !== null ? formatTime(event.endTime) : "";
const startDate = formatDate(event.startTime);
return (
<div
className={`border border-gray-200 rounded-lg ${paddingClass} ${clickableClass} transition-colors`}
onClick={onClick}
>
{/* Header with title and provenance */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<h3 className="text-base font-medium text-gray-900">{event.title}</h3>
{event.description && <p className="text-sm text-gray-600 mt-1">{event.description}</p>}
</div>
<ProvenanceIndicator
instanceId={provenance.instanceId}
instanceName={provenance.instanceName}
instanceUrl={provenance.instanceUrl}
compact={compact}
/>
</div>
{/* Metadata row */}
<div className="flex items-center gap-3 text-sm flex-wrap">
{/* Date */}
<span className="text-xs text-gray-700 font-medium">{startDate}</span>
{/* Time */}
{event.allDay ? (
<span className="text-xs text-gray-600">All day</span>
) : (
<span className="text-xs text-gray-600">
{startTime} - {endTime}
</span>
)}
{/* Location */}
{event.location && (
<span className="text-xs text-gray-600">
<span className="mr-1">📍</span>
{event.location}
</span>
)}
</div>
</div>
);
}