12 KiB
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 XcodeFchatiApp/Sources/FchatiApp/FchatiApp.swift— MenuBarExtra (.window style), 360pt wide, 480pt min height, three tabs (Chat / Pairing / Settings) with segmented pickerFchatiApp/FchatiApp.entitlements— sandbox + outgoing/incoming networkFchatiApp/Info.plist— bundle IDde.diyaa.fchati, version 0.1.0,LSUIElement = YES(hides Dock icon)FchatiApp/Tests/FchatiAppTests/AppTabTests.swift— verifies all three tabs and their icons existscripts/run-macos-app.sh— clean → test → release build → bundle → codesign → install to /Applications → open
Verified
swift buildpasses (debug)swift testpasses (1 test)- Script installs and opens
/Applications/Fchati.appsuccessfully
Fixes applied after agent review
- Added
LSUIElement = YEStoInfo.plist(was missing — app was appearing in Dock) - Added
--entitlementsflag tocodesignin 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 buildpasses fromFchatiApp/.
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-datawith the requiredfilefield 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 buildpasses fromFchatiApp/.swift testpasses 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 usingURLSessionWebSocketTask.- Connect sends authentication immediately, requires an
auth.okresponse within 10 seconds, and exposes authenticated incoming messages throughAsyncStream. - 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 buildpasses fromFchatiApp/.swift testpasses 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 buildpasses fromFchatiApp/.swift testpasses 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 buildpasses fromFchatiApp/.swift testpasses 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,.m4arecording 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.entitlementsandFchatiApp/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 buildpasses fromFchatiApp/.swift testpasses 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.peerNameand callsAppSession.unpair()to clear pairing credentials.
Verified
swift buildpasses fromFchatiApp/.swift testpasses with all current application tests.
Integration Update ✅
Completed by: Codex agent
What was updated
FchatiApp/Sources/FchatiApp/FchatiApp.swiftnow rendersPairingView,ChatView, andSettingsViewin 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 buildpasses fromFchatiApp/.swift testpasses 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 withFILE_TTL_DAYS.POST /admin/cleanup— protected manual cleanup endpoint. It accepts an optional JSONdaysvalue between 1 and 3650 and returns the number of deleted files and bytes.relay-server/.env.example— documents the retention setting and requiredADMIN_TOKENbearer secret.
Security
- The manual cleanup endpoint returns unavailable until
ADMIN_TOKENis configured. - Requests must use
Authorization: Bearer <ADMIN_TOKEN>.
Deployment configuration hardening
relay-server/.env.examplenow contains a non-secret placeholder forADMIN_TOKENinstead of a committed credential. Before deployment, generate and set a unique value in the untrackedrelay-server/.envfile.- 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 throughTRAEFIK_CERT_RESOLVER. - Corrected the production hostname to
fchaty.diyaa.dein the relay route and macOS application HTTP and WebSocket clients. relay-server/Dockerfileinitializes all persistent-data paths as the unprivilegednodeuser, 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— addedsentAt: String?field toWSMessageFchatiApp/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— handlepeer.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— addedmarkVisibleMessagesRead(): marks all incoming unread messages as read and sends{ type: "read", messageIDs: [...] }via WebSocketFchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift— callsmarkVisibleMessagesRead()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.