fchaty/IMPLEMENTATION_STATUS.md
2026-07-26 20:57:05 +02:00

10 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.

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).


Pending

Nothing remaining in the codebase. Next step: deploy relay server to production.