257 lines
12 KiB
Markdown
257 lines
12 KiB
Markdown
# 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 <ADMIN_TOKEN>`.
|
|
|
|
### 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.
|
|
- `FchatiApp/Resources/AppIcon.png` and `FchatiApp/Resources/Fchati.icns` provide the bundled Dock and application icon, and the installer copies the icon into the application bundle.
|
|
|
|
## 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.
|