# IndexedDB-Only DOM Optimization Plan
**Date:** 2025-11-12  
**Status:** Planning Phase  
**Goal:** Reduce DOM data-attributes to only event ID, using IndexedDB as single source of truth

## Current Problem

Events currently store all data in DOM attributes:
```html
<swp-event 
  data-event-id="123"
  data-title="Meeting"
  data-description="Long description..."
  data-start="2025-11-10T10:00:00Z"
  data-end="2025-11-10T11:00:00Z"
  data-type="work"
  data-duration="60"
/>
```

**Issues:**
- Data duplication (IndexedDB + DOM)
- Synchronization complexity
- Large DOM size with descriptions
- Memory overhead

## Proposed Solution

### Architecture Principle

**Single Source of Truth: IndexedDB**

```mermaid
graph TB
    A[IndexedDB] -->|getEvent| B[SwpEventElement]
    B -->|Only stores| C[data-event-id]
    B -->|Renders from| D[ICalendarEvent]
    A -->|Provides| D
```

### Target DOM Structure

```html
<swp-event data-event-id="123" />
```

Only 1 attribute instead of 8+.

## Implementation Plan

### Phase 1: Refactor SwpEventElement

**File:** `src/elements/SwpEventElement.ts`

#### 1.1 Remove Getters/Setters

Remove all property getters/setters except `eventId`:
- ❌ Remove: `start`, `end`, `title`, `description`, `type`
- ✅ Keep: `eventId`

#### 1.2 Add IndexedDB Reference

```typescript
export class SwpEventElement extends BaseSwpEventElement {
  private static indexedDB: IndexedDBService;
  
  static setIndexedDB(db: IndexedDBService): void {
    SwpEventElement.indexedDB = db;
  }
}
```

#### 1.3 Implement Async Data Loading

```typescript
async connectedCallback() {
  const event = await this.loadEventData();
  if (event) {
    await this.renderFromEvent(event);
  }
}

private async loadEventData(): Promise<ICalendarEvent | null> {
  return await SwpEventElement.indexedDB.getEvent(this.eventId);
}
```

#### 1.4 Update Render Method

```typescript
private async renderFromEvent(event: ICalendarEvent): Promise<void> {
  const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
  const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
  
  this.innerHTML = `
    <swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
    <swp-event-title>${event.title}</swp-event-title>
    ${event.description ? `<swp-event-description>${event.description}</swp-event-description>` : ''}
  `;
}
```

### Phase 2: Update Factory Method

**File:** `src/elements/SwpEventElement.ts` (line 284)

```typescript
public static fromCalendarEvent(event: ICalendarEvent): SwpEventElement {
  const element = document.createElement('swp-event') as SwpEventElement;
  
  // Only set event ID - all other data comes from IndexedDB
  element.dataset.eventId = event.id;
  
  return element;
}
```

### Phase 3: Update Extract Method

**File:** `src/elements/SwpEventElement.ts` (line 303)

```typescript
public static async extractCalendarEventFromElement(element: HTMLElement): Promise<ICalendarEvent | null> {
  const eventId = element.dataset.eventId;
  if (!eventId) return null;
  
  // Load from IndexedDB instead of reading from DOM
  return await SwpEventElement.indexedDB.getEvent(eventId);
}
```

### Phase 4: Update Position Updates

**File:** `src/elements/SwpEventElement.ts` (line 117)

```typescript
public async updatePosition(columnDate: Date, snappedY: number): Promise<void> {
  // 1. Update visual position
  this.style.top = `${snappedY + 1}px`;
  
  // 2. Load current event data from IndexedDB
  const event = await this.loadEventData();
  if (!event) return;
  
  // 3. Calculate new timestamps
  const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY, event);
  
  // 4. Create new dates
  const startDate = this.dateService.createDateAtTime(columnDate, startMinutes);
  let endDate = this.dateService.createDateAtTime(columnDate, endMinutes);
  
  // Handle cross-midnight
  if (endMinutes >= 1440) {
    const extraDays = Math.floor(endMinutes / 1440);
    endDate = this.dateService.addDays(endDate, extraDays);
  }
  
  // 5. Update in IndexedDB
  const updatedEvent = { ...event, start: startDate, end: endDate };
  await SwpEventElement.indexedDB.saveEvent(updatedEvent);
  
  // 6. Re-render from updated data
  await this.renderFromEvent(updatedEvent);
}
```

### Phase 5: Update Height Updates

**File:** `src/elements/SwpEventElement.ts` (line 142)

```typescript
public async updateHeight(newHeight: number): Promise<void> {
  // 1. Update visual height
  this.style.height = `${newHeight}px`;
  
  // 2. Load current event
  const event = await this.loadEventData();
  if (!event) return;
  
  // 3. Calculate new end time
  const gridSettings = this.config.gridSettings;
  const { hourHeight, snapInterval } = gridSettings;
  
  const rawDurationMinutes = (newHeight / hourHeight) * 60;
  const snappedDurationMinutes = Math.round(rawDurationMinutes / snapInterval) * snapInterval;
  
  const endDate = this.dateService.addMinutes(event.start, snappedDurationMinutes);
  
  // 4. Update in IndexedDB
  const updatedEvent = { ...event, end: endDate };
  await SwpEventElement.indexedDB.saveEvent(updatedEvent);
  
  // 5. Re-render
  await this.renderFromEvent(updatedEvent);
}
```

### Phase 6: Update Calculate Times

**File:** `src/elements/SwpEventElement.ts` (line 255)

```typescript
private calculateTimesFromPosition(snappedY: number, event: ICalendarEvent): { startMinutes: number; endMinutes: number } {
  const gridSettings = this.config.gridSettings;
  const { hourHeight, dayStartHour, snapInterval } = gridSettings;
  
  // Calculate original duration from event data
  const originalDuration = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
  
  // Calculate snapped start minutes
  const minutesFromGridStart = (snappedY / hourHeight) * 60;
  const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
  const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
  
  // Calculate end minutes
  const endMinutes = snappedStartMinutes + originalDuration;
  
  return { startMinutes: snappedStartMinutes, endMinutes };
}
```

### Phase 7: Update DragDropManager

**File:** `src/managers/DragDropManager.ts`

All places reading from `element.dataset.start`, `element.dataset.end` etc. must change to:

```typescript
// Before:
const start = new Date(element.dataset.start);
const end = new Date(element.dataset.end);

// After:
const event = await SwpEventElement.extractCalendarEventFromElement(element);
if (!event) return;
const start = event.start;
const end = event.end;
```

### Phase 8: Update Clone Method

**File:** `src/elements/SwpEventElement.ts` (line 169)

```typescript
public async createClone(): Promise<SwpEventElement> {
  const clone = this.cloneNode(true) as SwpEventElement;
  
  // Apply "clone-" prefix to ID
  clone.dataset.eventId = `clone-${this.eventId}`;
  
  // Disable pointer events
  clone.style.pointerEvents = 'none';
  
  // Load event data to get duration
  const event = await this.loadEventData();
  if (event) {
    const duration = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
    clone.dataset.originalDuration = duration.toString();
  }
  
  // Set height from original
  clone.style.height = this.style.height || `${this.getBoundingClientRect().height}px`;
  
  return clone;
}
```

### Phase 9: Initialize IndexedDB Reference

**File:** `src/index.ts`

```typescript
// After IndexedDB initialization
const indexedDB = new IndexedDBService();
await indexedDB.initialize();

// Set reference in SwpEventElement
SwpEventElement.setIndexedDB(indexedDB);
```

## Data Flow

```mermaid
sequenceDiagram
    participant DOM as SwpEventElement
    participant IDB as IndexedDBService
    participant User
    
    User->>DOM: Drag event
    DOM->>IDB: getEvent(id)
    IDB-->>DOM: ICalendarEvent
    DOM->>DOM: Calculate new position
    DOM->>IDB: saveEvent(updated)
    IDB-->>DOM: Success
    DOM->>DOM: renderFromEvent()
```

## Benefits

✅ **Minimal DOM**: Only 1 attribute instead of 8+  
✅ **Single Source of Truth**: IndexedDB is authoritative  
✅ **No Duplication**: Data only in one place  
✅ **Scalability**: Large descriptions no problem  
✅ **Simpler Sync**: No DOM/IndexedDB mismatch  

## Potential Challenges

⚠️ **Async Complexity**: All data operations become async  
⚠️ **Performance**: More IndexedDB lookups  
⚠️ **Drag Smoothness**: Async lookup during drag  

## Solutions to Challenges

1. **Async Complexity**: Use `async/await` consistently throughout
2. **Performance**: IndexedDB is fast enough for our use case
3. **Drag Smoothness**: Store `data-original-duration` during drag to avoid lookup

## Files to Modify

1. ✏️ `src/elements/SwpEventElement.ts` - Main refactoring
2. ✏️ `src/managers/DragDropManager.ts` - Update to use async lookups
3. ✏️ `src/index.ts` - Initialize IndexedDB reference
4. ✏️ `src/renderers/EventRenderer.ts` - May need async updates
5. ✏️ `src/managers/AllDayManager.ts` - May need async updates

## Testing Strategy

1. Test event rendering with only ID in DOM
2. Test drag & drop with async data loading
3. Test resize with async data loading
4. Test performance with many events
5. Test offline functionality
6. Test sync after reconnection

## Next Steps

1. Review this plan
2. Discuss any concerns or modifications
3. Switch to Code mode for implementation
4. Implement phase by phase
5. Test thoroughly after each phase

---

**Note:** This is a significant architectural change. We should implement it carefully and test thoroughly at each phase.