# Fchati — Current State & Tasks

> **Important:** The Mac app must be built entirely without opening Xcode.
> Use `Package.swift` (Swift Package Manager) as the project structure.
> All tasks are agent-ready: copy the prompt and send it directly to your coding agent.

---

## Rules for every agent

These rules apply to every agent that works on this project, without exception.

**1. No Arabic anywhere.**
All code, comments, string literals, log messages, error messages, and documentation files must be in English only.

**2. Mark your task as done.**
When you finish a task, find it in this file and change its status line from `[ ]` to `[x]` and add today's date. Example:
```
- [x] TASK-02 — KeychainStore  (done 2026-07-26)
```
This tells the next agent not to redo your work.

**3. Document what you did.**
Update `IMPLEMENTATION_STATUS.md` with: what files you created or changed, what was verified (tests, build), and anything you found missing or left incomplete. Write in English. This file is how the next session picks up exactly where you left off.

**4. Verify before finishing.**
Run `swift build` from `FchatiApp/` before declaring the task complete. If it does not compile, fix it. Do not leave broken code.

**5. Do not touch completed tasks.**
If a task is marked `[x]`, do not modify those files unless explicitly instructed.

---

## Architecture Decisions (source of truth)

These are decisions made during planning — do not change without revisiting the reasoning.

**Transport:** WebSocket (WSS) over Traefik. Traefik handles TLS via Let's Encrypt. The app uses `URLSessionWebSocketTask` — no custom TCP stack, no certificate pinning, no OpenSSL.

**Server role:** The relay is a dumb pipe. It does not store conversation history. It only holds undelivered messages temporarily (offline queue). Once delivered, messages are deleted from the server.

**Conversation history:** Stored locally on each device only. The app saves every message it sends or receives to a local JSON file. This is the permanent record — the server is not the archive.

**Offline delivery:** Messages sent to an offline peer are queued on disk on the server (not in RAM). When the peer reconnects, all queued messages are flushed immediately. Queue TTL: 7 days. Queue is disk-backed so it survives server restarts.

**Pairing sessions:** Also persisted on disk. Tokens survive server restarts — users never need to re-pair after a server update.

**Read receipts:** When a peer opens and reads messages, the app sends `{ type: "read", messageIDs: [...] }`. The server forwards this to the sender (or queues it if offline). The sender's app then shows "read" status on those messages.

**File storage:** Uploaded files (images, voice, documents) live on the server at `/data/files`. Text message JSON is negligible in size (a year of heavy messaging ≈ tens of MB). Files are the real storage concern — photos/voice accumulate at ~1–2 GB/month with heavy use.

**File cleanup:** Files older than 30 days should be auto-deleted by the server. A manual "Clean now" button will also be available in Settings. Text messages on the device are never auto-deleted.

**Server requirements:** A $5/month VPS (1 vCPU, 1 GB RAM) is more than sufficient for a small group of friends. The relay holds connections open but does no heavy processing.

**No iCloud, no third-party services:** Everything flows through the self-hosted relay. Apple has no access to messages.

**Mac app distribution:** Not via App Store. Distribution options TBD (direct `.app` download, notarization, or TestFlight).

---

## Current State

### Done

| Part | Details |
|------|---------|
| `relay-server/src/index.js` | Node.js relay — Pairing, WebSocket, offline queue (disk-backed), read receipts, file upload/download |
| `relay-server/Dockerfile` | Multi-stage Alpine build |
| `relay-server/docker-compose.yml` | Traefik labels for `fchaty.diyaa.de`, three persistent volumes (files, queue, pairing) |
| `relay-server/.env.example` | Environment variable template |
| Git repo | `git.mohfarawati.de/diyaa/fchaty` |

### Not built yet

The Mac app (`FchatiApp/`) does not exist yet.

### Pending server tasks

| Task | Details |
|------|---------|
| File auto-cleanup | ✅ Done 2026-07-26 — deletes files older than `FILE_TTL_DAYS` on startup and daily. |
| Manual cleanup endpoint | ✅ Done 2026-07-26 — protected `POST /admin/cleanup` deletes files older than a requested number of days. |
| Deploy to server | `git pull && docker compose up -d --build` on the production server. Not done yet — server is still being set up. |

---

## Target App Structure

```
f-chaty-native-new/
├── relay-server/                    ✅ done
└── FchatiApp/
    ├── Package.swift                no Xcode — SPM only
    ├── Sources/
    │   └── FchatiApp/
    │       ├── FchatiApp.swift
    │       ├── Session/
    │       │   └── AppSession.swift
    │       ├── Networking/
    │       │   ├── RelayAPI.swift
    │       │   ├── WSClient.swift
    │       │   └── MessageRouter.swift
    │       ├── Storage/
    │       │   ├── KeychainStore.swift
    │       │   └── MessageStore.swift
    │       └── Features/
    │           ├── Chat/
    │           │   ├── ChatView.swift
    │           │   ├── ChatViewModel.swift
    │           │   └── MessageBubble.swift
    │           ├── Pairing/
    │           │   ├── PairingView.swift
    │           │   └── PairingViewModel.swift
    │           └── Settings/
    │               └── SettingsView.swift
    └── FchatiApp.entitlements
```

---

## Tasks

> Each task is fully independent. Send any one to a coding agent without context from the others.

---

### TASK-01 — Swift Package + App Shell ✅ done 2026-07-26

**Completed.** See `IMPLEMENTATION_STATUS.md` for details.  
**Post-review fixes:** Added `LSUIElement` to `Info.plist`, applied entitlements in codesign script.

**Requirements:**

Create `FchatiApp/Package.swift`:
```swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "FchatiApp",
    platforms: [.macOS(.v14)],
    targets: [
        .executableTarget(
            name: "FchatiApp",
            path: "Sources/FchatiApp"
        )
    ]
)
```

Create `FchatiApp/Sources/FchatiApp/FchatiApp.swift`:
- `@main` SwiftUI App
- Uses `MenuBarExtra` with `.window` style
- Menu bar icon: `Image(systemName: "message")`
- Popover width: 360pt, min height: 480pt
- A root view with three tabs: Chat, Pairing, Settings (segmented picker at top)

Create `FchatiApp/FchatiApp.entitlements`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
</dict>
</plist>
```

Verify: `swift build` succeeds from `FchatiApp/` directory.

---

### TASK-02 — KeychainStore ✅ done 2026-07-26

**Reviewed:** No issues. Clean Security.framework usage, correct access policy (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`), `clearAll()` correctly preserves installationID.

---

### TASK-03 — RelayAPI (HTTP) ✅ done 2026-07-26

**Reviewed:** Good. All endpoints correct, multipart/form-data properly formatted, error types match spec.  
**Note for future:** `uploadFile` loads entire file into memory — acceptable for now, revisit if large video support is needed.

---

### TASK-04 — WSClient (WebSocket) ✅ done 2026-07-26

**Reviewed:** Excellent. Reconnect backoff correct, auth timeout clean, race conditions protected via task identity.  
**Fix applied:** Added `messageIDs: [String]?` and `attachment: WSAttachment?` to `WSMessage` — required for read receipts and file messages.

---

### TASK-05 — AppSession (State Machine)

- [x] TASK-05 — AppSession (State Machine) (done 2026-07-26)

**Priority:** Third — after TASK-02, TASK-03, TASK-04  
**File:** `FchatiApp/Sources/FchatiApp/Session/AppSession.swift`  
**Depends on:** `KeychainStore`, `RelayAPI`, `WSClient`

**Requirements:**

```swift
enum AppState: Equatable {
    case unpaired
    case connecting
    case connected(peerID: String, peerName: String)
    case error(String)
}

@MainActor
final class AppSession: ObservableObject {
    static let shared = AppSession()
    
    @Published var state: AppState = .unpaired
    @Published var messages: [ChatMessage] = []
    
    // Pairing
    func createPairingCode(displayName: String) async throws -> String  // returns code
    func joinWithCode(_ code: String, displayName: String) async throws
    
    // Messaging
    func sendText(_ body: String) async throws
    func sendFile(url: URL) async throws
    
    // Lifecycle
    func unpair()
}
```

On init: read token from `KeychainStore`. If present, attempt `WSClient.connect` immediately.  
On incoming `chat.message`: append to `messages` and trigger a `UNUserNotification` if app is in background.

---

### TASK-06 — PairingView

- [x] TASK-06 — PairingView (done 2026-07-26)

**Priority:** Fourth — after TASK-05  
**Files:** `Features/Pairing/PairingView.swift`, `PairingViewModel.swift`

**Requirements:**

Two modes switchable via a segmented picker: "Create Code" / "Enter Code".

**Create Code mode:**
- Text field for display name
- "Create Code" button → calls `AppSession.shared.createPairingCode`
- Shows the returned code (e.g. `FCHT-AB3K7Q`) in a monospaced font with a Copy button
- Shows "Code expires in 5 minutes"
- Shows a spinner while waiting for the other peer to join

**Enter Code mode:**
- Text field for display name  
- Text field for code — auto-uppercases, accepts with or without `FCHT-` prefix
- "Connect" button → calls `AppSession.shared.joinWithCode`
- Shows inline error if code is wrong or expired

Both modes observe `AppSession.shared.state` and navigate to `ChatView` when state becomes `.connected`.

---

### TASK-07 — ChatView

- [x] TASK-07 — ChatView (done 2026-07-26)

**Priority:** Fourth (parallel with TASK-06)  
**Files:** `Features/Chat/ChatView.swift`, `ChatViewModel.swift`, `MessageBubble.swift`

**Requirements:**

`MessageBubble`:
- Sent messages: right-aligned, blue bubble
- Received messages: left-aligned, gray bubble
- Render message body using `AttributedString(markdown:)` for Markdown support
- Show timestamp below bubble

`ChatView`:
- `ScrollViewReader` with auto-scroll to latest message
- Message list from `AppSession.shared.messages`
- Text input bar at bottom + Send button (also triggers on Return key)
- Paperclip button 📎 opens `NSOpenPanel` for file selection → calls `AppSession.shared.sendFile`
- Microphone button 🎙 — hold to record (`AVAudioRecorder`, `.m4a`), release to send as file

---

### TASK-08 — MessageStore (Local Persistence)

- [x] TASK-08 — MessageStore (Local Persistence) (done 2026-07-26)

**Priority:** Fourth (parallel)  
**File:** `FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift`  
**Independent:** Yes

**Requirements:**

```swift
struct ChatMessage: Codable, Identifiable, Equatable {
    let id: String
    let from: String
    let fromName: String
    let body: String
    let sentAt: Date
    let attachment: AttachmentInfo?
    var isRead: Bool
}

struct AttachmentInfo: Codable, Equatable {
    let fileID: String
    let name: String
    let size: Int
}

actor MessageStore {
    static let shared: MessageStore
    
    func save(_ message: ChatMessage) throws
    func loadAll() throws -> [ChatMessage]
    func markRead(id: String) throws
    var unreadCount: Int { get async }
}
```

Storage: JSON file at `FileManager.default.urls(for: .applicationSupportDirectory, ...).first!.appendingPathComponent("de.diyaa.fchati/messages.json")`.  
Create the directory if it does not exist.

---

### TASK-09 — SettingsView

- [x] TASK-09 — SettingsView (done 2026-07-26)

**Priority:** Fifth  
**File:** `Features/Settings/SettingsView.swift`

**Requirements:**

Simple settings screen:
- Display current user name (read from `KeychainStore.peerName`) with an Edit button
- Show connection status: "Connected to [peer name]" or "Not connected"
- "Unpair" button → calls `AppSession.shared.unpair()` and clears Keychain
- App version at the bottom (read from `Bundle.main.infoDictionary["CFBundleShortVersionString"]`)

---

### TASK-10 — New Message Notifications

- [x] TASK-10 — New Message Notifications (done 2026-07-26)

**Priority:** Fifth (independent)  
**File:** `FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift`

**Requirements:**

```swift
final class NotificationManager {
    static let shared = NotificationManager()
    
    func requestPermission() async
    func notify(from senderName: String, body: String)
}
```

- `requestPermission()` called once on first launch
- `notify` fires a `UNUserNotificationRequest` with title = sender name, body = first 100 chars of message body
- Do not send a notification if the menu bar popover is currently visible

---

## How to Send a Task to a Coding Agent

Copy the block below, fill in the task number and paste it to your agent:

```
Context:
- Repo: https://git.mohfarawati.de/diyaa/fchaty.git
- App folder: FchatiApp/ (Swift Package, no Xcode, SPM only)
- macOS 14+, Swift 5.9+, no third-party libraries
- Relay server is already running at https://fchaty.diyaa.de

Task: [paste the full task section here, e.g. TASK-04]

After writing the code:
1. Make sure it compiles: run `swift build` from the FchatiApp/ directory
2. Fix any compiler errors before finishing
3. Do not open Xcode or create any .xcodeproj or .xcworkspace files
```
