# Adding a module

A walk-through for adding a 7th module — say, `ISafariSync` for syncing
Safari bookmarks — following the patterns already in place for the six
shipping modules. Every step cites where the equivalent already lives.

## 0. Decide direction and OS surface

- **Direction**: read-only (like iPhoto), 2-way via SendQueueClient (like
  iCal), or 2-way via AppleScript (like iNotes)?
- **OS surface**: which framework? Safari bookmarks live in
  `~/Library/Safari/Bookmarks.plist`; assume a plist reader for this example.
- **Interval**: how often to tick? Cheap reads (EventKit) every 300s; slow
  AppleScript every 600s; near-realtime queries (chat.db) every 30s.

Document this in a one-line summary you can paste into the README table.

## 1. SwiftPM target

Add a target block in `Package.swift` modelled on
`Package.swift:80-87` (`IReminderSync` is the closest analogue):

```swift
.target(
    name: "ISafariSync",
    dependencies: ["MacSyncShared"],
    path: "@packages/isafari/Sources/ISafariSync"
    // linkerSettings only if you need a framework like EventKit
),
.testTarget(
    name: "ISafariSyncTests",
    dependencies: ["ISafariSync"],
    path: "@packages/isafari/Tests/ISafariSyncTests"
),
```

Add `"ISafariSync"` to the `MacSyncApp` executable dependencies block
(`Package.swift:97-110`).

## 2. Module source files

Create four files under `@packages/isafari/Sources/ISafariSync/`:

| File              | Role                                                | Example to copy                                                              |
|-------------------|-----------------------------------------------------|------------------------------------------------------------------------------|
| `Reader.swift`    | Talks to the OS (plist parse, EventKit, AppleScript)| `@packages/inotes/Sources/INoteSync/Reader.swift`                            |
| `APIClient.swift` | Typed HTTPS calls over `BaseAPIClient`              | `@packages/inotes/Sources/INoteSync/APIClient.swift` (esp. lines 60-130)     |
| `SyncManager.swift`| Subclass of `BaseSyncManager<Stats, Error>`        | `@packages/inotes/Sources/INoteSync/SyncManager.swift` (full file)           |
| `Sender.swift`    | Only if bidirectional                               | `@packages/ical/Sources/ICalSync/Sender.swift`                               |

Skeleton for `SyncManager.swift`:

```swift
import MacSyncShared

public struct ISafariSyncStats: Equatable, Sendable {
    public var bookmarkCount: Int = 0
    public init() {}
}

public enum ISafariSyncError: Equatable, Sendable {
    case none
    case bookmarksUnavailable
    case connectionFailed(String)
}

@MainActor
public final class SyncManager: BaseSyncManager<ISafariSyncStats, ISafariSyncError> {
    public static let shared = SyncManager()
    private init() {
        super.init(
            initialStats: ISafariSyncStats(),
            noError: .none,
            persistenceKey: "isafari",  // becomes the UserDefaults watermark namespace
            timerInterval: 600
        )
    }
    public override func performSync() async {
        // Reader.fetch -> APIClient.sync(payloads) -> stats update
    }
}
```

If bidirectional, also override `didStartSync()` /  `willStopSync()` and own a
`lazy var sendQueueClient: SendQueueClient<SafariSendTransport>`. The two
existing patterns:

- EventKit:  `@packages/ical/Sources/ICalSync/SyncManager.swift:54-73`
- AppleScript: `@packages/inotes/Sources/INoteSync/SyncManager.swift:53-72`

## 3. Server entities, features, surfaces

The repo follows a strict three-layer split:

- `src/server/src/entities/<entity>/` — schema migrations, types, repo
- `src/server/src/features/<module>/` — service-level functions (`syncBookmarks`, `queryBookmarks`)
- `src/server/src/surfaces/{client,my,admin}/<module>.ts` — HTTP routes

Files to create for read-only iSafari (mirror `inotes/`):

```
src/server/src/entities/bookmark/{schema.ts,types.ts,repo.ts,index.ts}
src/server/src/features/isafari/{index.ts,syncService.ts}
src/server/src/surfaces/client/isafari.ts
src/server/src/surfaces/my/bookmarks.ts
```

If bidirectional, also create:

```
src/server/src/entities/bookmarkSendItem/{schema.ts,types.ts,index.ts}
src/server/src/features/isafari/sendService.ts
src/server/src/surfaces/admin/bookmark-send-queue.ts
```

`bookmarkSendItem/schema.ts` reuses the table-builder
(`src/server/src/entities/noteSendItem/schema.ts:6-8` for the pattern):

```ts
import { sendQueueTableSql } from '@/shared/sendQueue/SendQueueRepo';

export const bookmarkSendItemMigrations = [
  { id: '2026-05-13_bookmark_send_queue_initial',
    sql: sendQueueTableSql('icloud.bookmark_send_queue') },
];
```

`features/isafari/sendService.ts` (mirroring
`src/server/src/features/inotes/sendService.ts:11-19`):

```ts
const BOOKMARK_SEND_ACTIONS = ['create_bookmark', 'update_bookmark', 'delete_bookmark'] as const;
const payloadSchema = z.object({ /* ... */ });
const repo = createSendQueueRepo({
  tableName: 'icloud.bookmark_send_queue',
  payloadSchema,
  allowedActions: BOOKMARK_SEND_ACTIONS,
});
```

## 4. Wire the routes

Three index files need a one-line edit each (the rest of the surface tree is
discovered automatically):

- `src/server/src/surfaces/client/index.ts:11-19` — add
  `.route('/isafari', isafariClientRouter)`
- `src/server/src/surfaces/my/index.ts:11-19` — add
  `.route('/bookmarks', bookmarksMyRouter)`
- `src/server/src/surfaces/admin/index.ts:10-17` — add
  `.route('/bookmark-send-queue', bookmarkSendQueueAdminRouter)` if bidirectional

Migrations also need registration in `src/server/src/app/server.ts:36-52`:

```ts
...bookmarkMigrations,
...bookmarkSendItemMigrations,  // if bidirectional
```

## 5. Web tab

Create `web/src/api/bookmarks.ts` and `web/src/tabs/Bookmarks/index.tsx`,
modelled on `web/src/api/notes.ts` and `web/src/tabs/Notes/index.tsx`. Then
add a route in `web/src/App.tsx:55-65`:

```tsx
const BookmarksTab = lazy(() =>
  import('./tabs/Bookmarks').then((m) => ({ default: m.BookmarksTab })),
);
// inside <Routes>:
<Route path="/bookmarks" element={<BookmarksTab />} />
```

And a nav entry inside `web/src/layout/AppShell` (not pasted here — copy the
Notes entry).

## 6. App manifest & docs

- `app.manifest.yaml:6-12` — append `- isafari` to the `modules:` list.
- `CLAUDE.md:7-29` — extend the structure block.
- Add `docs/modules/isafari.md` modelled on
  `docs/modules/inotes.md`.

## 7. Tests

Mirror an existing module's `Tests/<Module>SyncTests/` directory. Hermetic
test coverage is limited to what does not need the OS — see
[modules/inotes.md](./modules/inotes.md#tests) for the breakdown.

For the server, follow `src/server/src/shared/sendQueue/__tests__/SendQueueRepo.test.ts`
(plan reference: `~/.claude/plans/magical-tumbling-peach.md:66`) for
queue round-trip tests against a test database.

## Checklist

- [ ] `Package.swift` target + executable dep
- [ ] `Reader.swift`, `APIClient.swift`, `SyncManager.swift` (+ `Sender.swift`)
- [ ] Server `entities/<x>/` migration + types + repo
- [ ] Server `features/<x>/` service
- [ ] Server `surfaces/client/<x>.ts`, `surfaces/my/<x>.ts` (+ admin if bidirectional)
- [ ] Migrations registered in `app/server.ts`
- [ ] Routes registered in three `surfaces/*/index.ts`
- [ ] Web `api/<x>.ts`, `tabs/<X>/index.tsx`, App.tsx route, AppShell nav entry
- [ ] `app.manifest.yaml` module list
- [ ] `docs/modules/<x>.md`
- [ ] Tests: `@packages/<x>/Tests/` + `src/server/src/features/<x>/__tests__/`
- [ ] `swift test` green, `bun test` green, `bun run typecheck` green
