fchaty/IMPLEMENTATION_STATUS.md
2026-07-27 02:45:51 +02:00

13 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 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 now replaces the app icon and provides the custom menu-bar icon through the Swift package resources; the installer copies that resource bundle 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.