# Implementation Status ## TASK-01 — Swift Package + App Shell ✅ **Completed by:** Codex agent **Fixes applied by:** Claude (LSUIElement, entitlements in codesign) ### What was built - `FchatiApp/Package.swift` — SPM package, macOS 14+, no Xcode - `FchatiApp/Sources/FchatiApp/FchatiApp.swift` — MenuBarExtra (.window style), 360pt wide, 480pt min height, three tabs (Chat / Pairing / Settings) with segmented picker - `FchatiApp/FchatiApp.entitlements` — sandbox + outgoing/incoming network - `FchatiApp/Info.plist` — bundle ID `de.diyaa.fchati`, version 0.1.0, `LSUIElement = YES` (hides Dock icon) - `FchatiApp/Tests/FchatiAppTests/AppTabTests.swift` — verifies all three tabs and their icons exist - `scripts/run-macos-app.sh` — clean → test → release build → bundle → codesign → install to /Applications → open ### Verified - `swift build` passes (debug) - `swift test` passes (1 test) - Script installs and opens `/Applications/Fchati.app` successfully ### Fixes applied after agent review - Added `LSUIElement = YES` to `Info.plist` (was missing — app was appearing in Dock) - Added `--entitlements` flag to `codesign` in the build script (sandbox permissions were not being applied) --- ## TASK-02 — KeychainStore ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Storage/KeychainStore.swift` — direct Security.framework wrapper for the installation ID, auth token, peer ID, and peer name. - The installation ID is created once as a UUID and persisted in Keychain. - Token and peer properties support read, update, and deletion through Swift property syntax. - `clearAll()` removes pairing credentials while preserving the installation ID. ### Verified - `swift build` passes from `FchatiApp/`. ## TASK-03 — RelayAPI (HTTP) ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Networking/RelayAPI.swift` — actor-based HTTP client for pairing creation, pairing join, file upload, and file download. - Pairing requests use JSON and decode the exact relay response payloads. - File uploads use `multipart/form-data` with the required `file` field and bearer-token authorization. - Non-success HTTP responses, transport failures, and response decoding failures map to `RelayAPIError`. - `FchatiApp/Tests/FchatiAppTests/RelayAPIResponseTests.swift` — response-decoding coverage for relay pairing and upload payloads. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with the relay response tests. ## TASK-04 — WSClient (WebSocket) ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Networking/WSClient.swift` — actor-based WebSocket client using `URLSessionWebSocketTask`. - Connect sends authentication immediately, requires an `auth.ok` response within 10 seconds, and exposes authenticated incoming messages through `AsyncStream`. - The client sends a ping every 30 seconds and reconnects after unexpected disconnects with delays of 2, 4, 8, 16, and 32 seconds. - Explicit disconnects cancel reconnect attempts and retain the incoming stream for a later connection. - `FchatiApp/Tests/FchatiAppTests/WSMessageTests.swift` — serialization coverage for relay WebSocket messages. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with the WebSocket message test. ## TASK-05 — AppSession (State Machine) ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — main-actor application state for pairing, session restoration, message sending, file sending, read receipts, and incoming message handling. - Existing credentials restore the WebSocket connection at launch. - Pairing and message failures update the published state with an error description. - File uploads are converted to attachment metadata and forwarded through relay chat messages. ## TASK-08 — MessageStore (Local Persistence) ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift` — actor-backed JSON persistence at the required Application Support location. - Message writes are atomic, deduplicated by message ID, sorted by timestamp, and retain read-receipt state. - `FchatiApp/Tests/FchatiAppTests/MessageStoreModelTests.swift` — serialization coverage for messages and attachment metadata. ## TASK-10 — New Message Notifications ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift` — notification permission request and background message notification delivery. - Notifications use the sender name, limit the preview to 100 characters, and are suppressed while the app is active. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with session storage coverage. ## TASK-06 — PairingView ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Features/Pairing/PairingView.swift` — create and join pairing screens with a segmented mode selector, code copy action, loading states, and inline errors. - `FchatiApp/Sources/FchatiApp/Features/Pairing/PairingViewModel.swift` — pairing request orchestration, code normalization, and display-state management. - Join codes are uppercased automatically and accept values with or without the `FCHT-` prefix. - A connection callback allows the app shell to switch to the chat screen when pairing succeeds. - `FchatiApp/Tests/FchatiAppTests/PairingViewModelTests.swift` — normalization coverage for pairing code input. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with pairing view-model coverage. ## TASK-07 — ChatView ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` — scrollable chat UI with automatic scrolling, text submission on Return, file selection, and hold-to-record voice input. - `FchatiApp/Sources/FchatiApp/Features/Chat/ChatViewModel.swift` — message and file sending, `.m4a` recording lifecycle, temporary recording cleanup, and inline send errors. - `FchatiApp/Sources/FchatiApp/Features/Chat/MessageBubble.swift` — left and right message bubbles, Markdown body rendering, attachment metadata, and timestamps. - `FchatiApp/FchatiApp.entitlements` and `FchatiApp/Info.plist` — microphone sandbox entitlement and privacy usage description required for voice recording. - `FchatiApp/Tests/FchatiAppTests/ChatMessageBubbleTests.swift` — Markdown rendering coverage for message bodies. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with chat view coverage. ## TASK-09 — SettingsView ✅ **Completed by:** Codex agent ### What was built - `FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift` — editable display name, published session connection state, destructive unpair action, and bundled application version display. - The view reads and writes the display name through `KeychainStore.peerName` and calls `AppSession.unpair()` to clear pairing credentials. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with all current application tests. ## Integration Update ✅ **Completed by:** Codex agent ### What was updated - `FchatiApp/Sources/FchatiApp/FchatiApp.swift` now renders `PairingView`, `ChatView`, and `SettingsView` in the menu-bar tabs instead of placeholder content. - The initial tab is pairing for first-time setup. - A successful pairing switches the selected tab to chat. ### Verified - `swift build` passes from `FchatiApp/`. - `swift test` passes with all current application tests. ## Server File Cleanup ✅ **Completed by:** Codex agent ### What was built - `relay-server/src/index.js` — removes expired uploaded files at startup and once every 24 hours. The retention period defaults to 30 days and is configurable with `FILE_TTL_DAYS`. - `POST /admin/cleanup` — protected manual cleanup endpoint. It accepts an optional JSON `days` value between 1 and 3650 and returns the number of deleted files and bytes. - `relay-server/.env.example` — documents the retention setting and required `ADMIN_TOKEN` bearer secret. ### Security - The manual cleanup endpoint returns unavailable until `ADMIN_TOKEN` is configured. - Requests must use `Authorization: Bearer `. ### Deployment configuration hardening - `relay-server/.env.example` now contains a non-secret placeholder for `ADMIN_TOKEN` instead of a committed credential. Before deployment, generate and set a unique value in the untracked `relay-server/.env` file. - Verified the Docker Compose configuration with the example environment and confirmed a local relay instance responds successfully from `GET /health`. - Extended WebSocket serialization coverage to preserve the sender timestamp used for offline message delivery. - The relay now defaults to the production Traefik certificate resolver (`myresolver`) and allows a host-specific override through `TRAEFIK_CERT_RESOLVER`. - Corrected the production hostname to `fchaty.diyaa.de` in the relay route and macOS application HTTP and WebSocket clients. - `relay-server/Dockerfile` initializes all persistent-data paths as the unprivileged `node` user, preventing pairing, queue, and upload write failures on newly created volumes. - The macOS app now has an always-visible quit control beside the top-level tabs, in addition to the existing Settings action. - The macOS app now includes a normal main window and Dock presence while retaining the menu-bar shortcut. ## Bug Fixes — Post-Review ✅ **Applied by:** Claude ### Fix 1 — `isSent` detection in ChatView **File:** `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` **Problem:** `MessageBubble` was deciding which side to render on using `message.fromName == "You"`. If the user ever set their display name to "You" this would misidentify received messages as sent. **Fix:** Compare `message.from == KeychainStore.installationID` — the installation UUID is unique per device and never changes. --- ### Fix 2 — Message timestamps for queued/offline messages **Files:** - `FchatiApp/Sources/FchatiApp/Networking/WSClient.swift` — added `sentAt: String?` field to `WSMessage` - `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — sender embeds ISO-8601 timestamp; receiver parses it **Problem:** When a message was delivered from the offline queue, `sentAt` was set to `Date()` (arrival time). Messages sent at 11pm but delivered after a restart showed a wrong timestamp. **Fix:** The sender now includes `sentAt` as an ISO-8601 string in the WebSocket payload. The relay passes it through unchanged (it's a dumb pipe). The receiver parses it; falls back to `Date()` only if the field is missing (old clients). --- ## Bug Fixes — Full Code Audit ✅ **Applied by:** Claude ### Fix 3 — Creator never reaches `.connected` state (critical) **Files:** - `relay-server/src/index.js` — after WebSocket auth, notify the partner with `{ type: "peer.joined", peerID, peerName }` - `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — handle `peer.joined`: save peerID/peerName to Keychain, transition state to `.connected` **Problem:** When the creator generates a pairing code and connects via WebSocket, their state gets stuck at `.connecting` forever. The server never told the creator that the joiner joined — it only responded to the joiner's HTTP request. The creator had no way to learn the joiner's identity or transition to `.connected`. **Fix:** Server now sends `{ type: "peer.joined", peerID, peerName }` to the online partner whenever any peer authenticates on WebSocket. Client handles this event and saves credentials + transitions state. --- ### Fix 4 — Read receipts were never sent (important) **Files:** - `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — added `markVisibleMessagesRead()`: marks all incoming unread messages as read and sends `{ type: "read", messageIDs: [...] }` via WebSocket - `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` — calls `markVisibleMessagesRead()` on appear and when new messages arrive **Problem:** The code handled *incoming* read receipts correctly but never *sent* them. The partner's messages were never acknowledged as read, so the partner never saw "read" status on their messages. ## Pending Nothing remaining.