fchaty/IMPLEMENTATION_STATUS.md
2026-07-27 02:55:08 +02:00

258 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.
- The supplied Teamwork Chat icon replaces the app icon; the menu bar uses the native monochrome message symbol for visual consistency with macOS.
## 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.