Skip to content

fix(tests): add missing TestResources.swift to restore CI#104

Open
martinmarsian wants to merge 89 commits into
nark:mainfrom
martinmarsian:fix/test-resources-missing
Open

fix(tests): add missing TestResources.swift to restore CI#104
martinmarsian wants to merge 89 commits into
nark:mainfrom
martinmarsian:fix/test-resources-missing

Conversation

@martinmarsian
Copy link
Copy Markdown

Summary

  • CompatibilityTests and P7SpecMetadataTests reference TestResources.specURL (introduced in a prior PR) but the file itself was never included in that merge, breaking CI on main since 2026-05-09
  • TestResources.swift resolves wired.xml via #filePath — works on macOS xctest and Linux SPM without depending on the SPM resource bundle

Test plan

  • CI passes on Linux and macOS after merge

🤖 Generated with Claude Code

Professor© and others added 30 commits April 24, 2026 14:24
Adds a complete migration path from Wired 2.5 SQLite databases into
the Wired 3 GRDB schema, covering accounts, privileges, bans, boards,
threads and posts.

Server CLI:
- New --migrate-from <path> flag in main.swift
- MigrationController: migrates groups+privileges, users+privileges,
  bans, boards (Unix mode → 6 permission booleans), threads, posts
  (BLOB icon support, date string → Unix epoch conversion)
- DatabaseController: expose dbQueue as public for migration access

WiredServerApp (macOS GUI):
- New "Migration" tab in preferences
- Source path picker + overwrite toggle + progress output view
- Subprocess runs wired3 --migrate-from via temp-file output capture
  (avoids pipe buffering / race-condition issues)
- Localizable strings added (de/en/fr)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accounts migrated from Wired 2.5 have password_salt = NULL and a 40-char
SHA1 hash stored in the password column. The P7 key exchange previously
always derived the auth proof from SHA256(plaintext), causing these accounts
to fail authentication entirely.

P7 protocol (P7 v1.4 additions):
- New field p7.encryption.legacy_password (bool, id=22) in server_challenge:
  server sets this when password.count==40 && salt==nil
- New field wired.user.password_must_change (bool, id=3016) in wired.login:
  signals the client to prompt for a password change immediately

P7Socket (server side):
- Detect legacy users after username_request; send legacy_password=true
  in server_challenge and set isLegacyAuth=true after ECDSA verify passes
- Server-side base_hash uses the stored SHA1 string unchanged (no re-derive)

P7Socket (client side):
- Defer password hashing until after server_challenge is received
- Use SHA1 when legacy_password=true, SHA256 otherwise
- Set isLegacyAuth=true for downstream consumers

ServerController+Auth (wired.send_login handler):
- For legacy sessions (socket.isLegacyAuth): skip the redundant SHA256
  equality check (P7 ECDSA already verified the SHA1 credential)
- Lazily assign a new per-user stored_salt on first legacy login
- Include wired.user.password_must_change=true in wired.login reply

Connection (client library):
- New connectionRequiresPasswordChange(connection:) delegate method
- Called after login when wired.user.password_must_change is true

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The FTS5 table uses content='index' (external content). On restart after
a crash, old rows in the `index` table may have no matching FTS5 entries.
The row-level delete trigger then sends a 'delete' command for a non-
existent FTS5 rowid, causing SQLITE_IOERR (error 10) during cleanup.

Fix: in performFullRebuild, drop the index_ad trigger before the bulk
delete, then call FTS5 'rebuild' to re-sync the index from the current
content table, then restore the trigger. This is more correct than
relying on row-level triggers for bulk operations.

Add recoverIndexAndFTS5() as a fallback: drops FTS5 and all triggers,
clears the index table, and recreates everything from scratch. A
pendingRebuild flag ensures a fresh traversal follows automatically.

Also: auto-load notary profile from ~/.wired-notary in release scripts
so notarization works without manually exporting NOTARY_PROFILE each time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two changes to prevent SQLITE_IOERR on the first index rebuild after a
crash or unclean shutdown:

1. DatabaseController: run PRAGMA wal_checkpoint(TRUNCATE) right after
   opening the database. On a fresh start there are no active readers,
   so TRUNCATE safely merges and clears the WAL. This eliminates the
   root cause: accumulated partial FTS5 shadow-table writes from failed
   previous rebuild cycles sitting in the WAL.

2. IndexController: before the first rebuild traversal, probe FTS5 with
   an integrity-check write. If it throws (SQLITE_IOERR or any other
   error), recoverIndexAndFTS5() is called to drop and recreate the FTS5
   virtual table and triggers from scratch, then a fresh rebuild follows.
   This acts as a belt-and-suspenders guard for cases where the main DB
   file itself has FTS5 inconsistencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The regex previously rejected versions like "3.0-beta.21" because it
only matched numeric dot-separated segments. Allow an optional
pre-release suffix (-beta.N, -rc.N, -alpha.N, etc.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Assigning the salt on the very first legacy login caused a permanent
lockout on the second login: passwordSalt != nil triggered the SHA-256
auth path, which failed against the still-SHA1 stored hash.

The salt is now written only in receiveAccountChangePassword, i.e. when
the user actually completes the password-change flow.

Fixes: nark#72

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…B column

The old detection (password.count == 40 && passwordSalt == nil) was fragile
and would produce false positives in edge cases. A dedicated is_legacy boolean
column on the users table is the authoritative source of truth:

- DB migration v12: adds is_legacy (default false), back-fills existing SHA1
  accounts (no salt + 40-char hash) automatically
- User model: new isLegacy property with CodingKey is_legacy
- SocketPasswordDelegate: new isLegacyUser(username:) method replaces the
  length check in P7Socket.serverKeyExchange
- UsersController: implements isLegacyUser(username:)
- MigrationController: sets is_legacy=1 for all Wired 2.5 imported accounts
- receiveAccountChangePassword: clears is_legacy=false on successful upgrade

Addresses nark's review of PR nark#72.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MigrationController: replace raw table/column string interpolation in
  migratePrivileges() with a closed PrivilegeTarget enum (allowlist)
- WiredServerViewModel.runMigration(): replace DispatchQueue.global/main
  mixing with withCheckedContinuation + withTaskGroup; add 5-minute timeout
  so isMigrating can never be stuck true forever if the subprocess hangs
- migrate-from-wired25.py: document that boards/threads/posts are out of
  scope; point users to wired3 --migrate-from for full content migration
- Add MigrationControllerTests: 8 tests covering is_legacy flag, skip/
  overwrite behaviour, legacy auth detection, and password upgrade flow

Addresses nark's minor review points on PR nark#72.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When recoverIndexAndFTS5() fails, hasFTS5 is set to false but the
index_ad trigger remains in the database. On the next rebuild the
trigger drop was skipped (gated on hasFTS5), the trigger fired during
the bulk DELETE, hit the corrupt FTS5 shadow tables, and propagated
SQLITE_IOERR (10) back to the DELETE statement.

DROP TRIGGER IF EXISTS index_ad is now unconditional so the trigger
is always gone before the bulk delete, regardless of hasFTS5 state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…offline user list

- wired.xml: add fields wired.user.is_offline (3017), wired.message.offline.{sender_login,recipient_login,date} (5010-5012); add messages wired.user.offline_list/done (3011-3012), wired.message.send_offline_message/offline_message (5010-5011)
- DB migration v13: replace speculative E2E-crypto offline_messages schema with a simple plaintext table (sender_login, recipient_login, body, sent_at) with per-recipient limit trigger
- OfflineMessage.swift: new GRDB model for the offline_messages table
- ServerController+Chat: receiveMessageSendOfflineMessage, deliverOfflineMessages, sendOfflineUserList
- ServerController+Auth: call sendOfflineUserList + deliverOfflineMessages after successful login
- ServerController: route wired.message.send_offline_message to new handler
- ClientsController: add allConnectedLogins() and user(withLogin:)
- UsersController: add userExists(withUsername:)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, privilege backfill

- Migration v14: add last_login_at to users; update on every successful login
- sendOfflineUserList: filter to users active in last 30 days; send full_name as nick
- broadcastNewOfflineUser: send full_name as nick when new account is created
- migrateSendOfflineMessagesPrivilegeIfNeeded: backfill privilege to existing accounts
  that already have send_messages, so upgrading installations get it automatically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… list

- Migration v15: add last_nick TEXT column to users table
- receiveUserSetNick: persist nick to DB whenever a logged-in user changes it
- Login path: also persist nick at login time (nick is sent before login completes)
- sendOfflineUserList: prefer last_nick, fall back to full_name, then username

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… user list

The IS NULL exception caused all accounts without a last_login_at (everyone
after a fresh upgrade) to appear. Now only accounts with a real login within
the last 30 days are shown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gin or full_name

Using login or full_name as fallback was a security issue — it revealed account
names to other users. Now only users who have connected at least once (and thus
have a last_nick persisted) appear in the offline list. New accounts are excluded
until their first login, at which point their self-chosen nick is saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ients

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Poly1305

- Migration v16: adds is_encrypted column to offline_messages
- wired.xml: fields 5014/5015 and messages 3013/3014/3015 for key exchange
- Server stores and delivers is_encrypted flag with each offline message
- Handler for wired.user.set_public_key stores X25519 public key in users.offline_public_key
- Handler for wired.user.get_public_key returns stored public key by login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GRDB stores Date values as ISO 8601 text ("yyyy-MM-dd HH:mm:ss.SSS"),
not as Unix timestamps. Reading sent_at via Date(timeIntervalSince1970:)
forced a Double conversion that fatalError'd on text values, crashing
the server whenever a user with pending offline messages logged in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry/Wired3/

- Persist workingDirectory in UserDefaults so it survives restarts after migration
- Add migrateToSystemDirectory() — stops server, creates /Library/Wired3/ with
  admin privileges (osascript), copies all data, verifies DB integrity (PRAGMA
  integrity_check), switches active path, rewrites LaunchAgent plist if needed
- Original data preserved as backup at ~/Library/Application Support/Wired3/
- Add systemMigrationAvailable, isUsingSystemDirectory computed properties
- Add WiredServerError cases: systemDirectoryCreationFailed, systemMigrationFailed
- Dashboard General tab: new "System Data Directory" section with migration button,
  progress indicator, and status line; shows green checkmark when already migrated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ServerInstallMode enum (launchAgent / launchDaemon), persisted in UserDefaults
- Add daemonUserName (default: _wired) and daemonStartAtBoot, both persisted
- switchInstallMode(): stops server, creates hidden system user via dscl (osascript),
  sets /Library/Wired3/ ownership, writes LaunchDaemon plist to /Library/LaunchDaemons/
  (via temp file + osascript cp with admin), or reverses all steps when switching back
- startDaemon() / stopDaemon(): launchctl bootstrap/bootout for system domain
- toggleDaemonStartAtBoot(): rewrites plist with updated RunAtLoad flag
- findFreeSystemUID(): auto-assigns UID from range 400-499
- Dashboard General tab: new "Install Mode" section with Picker, system user field
  with existence indicator, progress/status display; Execution section switches
  between LaunchAgent (Start at Login) and LaunchDaemon (Start at Boot) UI
- New WiredServerError cases: systemDirectoryOwnershipFailed, daemonUserCreationFailed,
  launchDaemonWriteFailed, launchDaemonInstallFailed, launchDaemonRemoveFailed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- activateLaunchDaemon(): single osascript call — user creation (optional) +
  chown /Library/Wired3 + plist copy to /Library/LaunchDaemons/ (one admin dialog)
- deactivateLaunchDaemon(): single osascript call — bootout + plist removal +
  chown back to current user (one admin dialog)
- reinstallDaemonPlist(): single osascript call for updating existing plist
  (used by toggleDaemonStartAtBoot)
- Extract runPrivileged() helper to avoid repeating osascript boilerplate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add daemonGroupName (default: staff), persisted in UserDefaults
- Add daemonGroupExists computed property
- findFreeSystemGID(): auto-assigns GID from range 400-499
- groupGID(for:): reads PrimaryGroupID from dscl for existing groups
- activateLaunchDaemon: creates custom group via dscl if it doesn't exist,
  uses correct GID for user's PrimaryGroupID, uses group name in chown
- saveDaemonSettings() persists group name alongside user name
- Dashboard: side-by-side User/Group text fields each with existence indicator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- filesDirectoryIsOnExternalVolume: detects if files dir is under /Volumes/
  using URLResourceKey.volumeURLKey
- wired3HasFullDiskAccess: queries TCC.db via sqlite3 for the wired3 binary's
  kTCCServiceSystemPolicyAllFiles entry (readable only if WiredServerApp also
  has FDA)
- openFullDiskAccessSettings(): opens System Settings Privacy_AllFiles pane
- ExternalVolumeWarningView: reusable warning banner — orange with
  "Open Settings…" button when FDA missing, green checkmark when granted
- Warning shown in "Install Mode" section (General tab) and in "Files" tab
  whenever LaunchDaemon mode is active and files dir is on external volume

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use Color.primary/Color.orange explicitly in foregroundStyle ternary to
  resolve HierarchicalShapeStyle vs Color type mismatch
- Replace URLResourceValues.volumeURL (unavailable in SDK) with simple
  /Volumes/ path prefix check for external volume detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fields were disabled when launchDaemonInstalled=true, making it
impossible to change user/group after the first mode switch. Now
the fields are always editable and shown regardless of install mode,
so user can configure them before or after switching to LaunchDaemon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace daemonRunning/daemonUserExists/daemonGroupExists computed
  properties (which ran launchctl/dscl synchronously on the main thread
  during every SwiftUI view render) with cached @published properties
  isDaemonRunning/isDaemonUserExists/isDaemonGroupExists
- Update these via refreshDaemonStatus() called from pollState() only
  when in LaunchDaemon mode (once per second, not on every render)
- Fix startDaemon()/stopDaemon() to use admin privileges via osascript
  since launchctl bootstrap/bootout system requires root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When in LaunchDaemon mode and the daemon is running, skip the automatic
binary synchronization in refreshAll(). launchd detects when the binary
file is replaced on disk and kills the running daemon process, which
appeared as a spurious server crash whenever the General Tab was opened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set chmod 775 on /Library/Wired3/bin/ after chown so admin user
  (in staff group) can update the wired3 binary without sudo
- Guard auto-update in refreshAll() with isWritableFile check so it
  silently skips when permissions aren't available (e.g. before the
  first mode switch has set the correct group-write bit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e for admin

Data dirs under /Library/Wired3 are chowned to _wired:daemon.
bin/ is explicitly re-chowned to _wired:staff with 775 so the admin
user (member of staff, not daemon) can still update the binary.
Default daemonGroupName changed from staff to daemon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Professor© and others added 30 commits April 29, 2026 12:59
…ility

- release.sh: use $HOME/Downloads instead of hardcoded VM-shared path
- build-wired-server-app.sh: auto-select Xcode over Command Line Tools so
  x86_64 Swift compatibility libs are available for universal binary builds;
  make notarize_zip fail fast on Invalid status instead of proceeding to
  staple; document why flat executables cannot have tickets stapled
- wired3: add --check-access <path> flag for lightweight FDA probe (exits 0/1,
  no server startup) used by WiredServerApp for the FDA status indicator
- WiredServerApp: FDA check script now runs the actual wired3 binary instead
  of /bin/ls; the TCC grant is bound to the wired3 binary path, so /bin/ls
  would fail on macOS 26+ even when FDA is correctly granted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use compile-time arch detection (#if arch(arm64) / arch(x86_64)) so that
wired.info.arch reflects the real binary architecture at runtime — arm64
on Apple Silicon and x86_64 on Intel. Previously all macOS builds
reported x86_64 regardless of which slice was executing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TextField(value:formatter:) bound directly to a @published Int caused
SwiftUI to re-render and overwrite the field content mid-edit whenever
any other @published property (e.g. portStatus from checkPort()) changed.
This manifested as the first digit of the default port 4871 ("4")
appearing unexpectedly when the user started typing a new port.

Replace with a @State string buffer (portText) that is only synced to
model.serverPort on explicit Save or Return. Non-numeric characters are
filtered on every keystroke. The model is updated only once on commit,
eliminating all formatter-driven re-render interference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the local NWConnection loopback probe (which only tested if the
server was listening on 127.0.0.1) with a two-step internet reachability
check: fetch external IP via api.ipify.org, then probe TCP reachability
via check-host.net. Open ports resolve in ~3 s, timeouts in ~6-13 s.

Port status gains a new .error case (orange) for when no internet is
available. Localization updated in all three languages with
internet-reachability semantics.

Network tab UI: port number field fixed-width (70 px) with "Default: 4871"
hint inline, stable layout using SettingsSection (GroupBox) instead of
Form .grouped to avoid macOS Form's floating-label and alignment behavior.
Port TextField uses a local @State String buffer so model re-renders don't
overwrite mid-edit text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Localization.swift only knew 'en' and 'fr' — 'de' was missing from the
AppLanguage enum, tableByLanguage dict, and resolveLanguage() check.

Completed the German strings file: added all missing keys (dashboard,
database, snapshot, touchid, identity, security, status, error messages)
and fixed stale keys (pane.advanced → pane.security, advanced.save →
security.save).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With max_nodes=1, a single randomly-assigned node timing out or being
slow caused false-negative "not reachable" results even when the port
was genuinely open. Now uses 3 nodes and returns open if any succeeds,
continuing to poll while any node is still pending.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
candidateBundles() was searching Bundle.main for .bundle files, which
doesn't reliably find the SPM-generated WiredSwift_WiredServerApp.bundle
that contains the localized .strings resources. Bundle.module is the
correct SPM accessor and always resolves to the right resource bundle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundle.main.preferredLocalizations was prepended to the candidate list.
For a plain SPM executable (no .app bundle), it returns ["en"] — so the
loop matched "en" before ever reaching the user's "de" preference from
Locale.preferredLanguages. Using only Locale.preferredLanguages ensures
the user's actual system language is always respected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SecureField was bound directly to model.newAdminPassword. Re-renders
triggered by other @published model properties could cause the binding
to lose the typed value before the button action read it. Using a local
@State var passwordText mirrors the fix applied to the port number field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tyle

SecureField inside Form .grouped renders without a visible border on
macOS, making it look like the field doesn't exist. Applying
.textFieldStyle(.roundedBorder) makes it explicitly visible and
interactable, consistent with other text inputs in the app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LaunchDaemon starts at boot before user login. Paths under /Users/ may
not be mounted or decrypted yet (FileVault), making them inaccessible
to the daemon. Show an orange warning label in the Files tab when this
combination is detected. Localized in en/de/fr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in protocol spec

The privilege was checked in sendOfflineUserList() but was never declared
as a field in wired.xml and not included in the wired.account.privileges
collection. This meant seedDefaultDataIfNeeded() never seeded it for the
admin account on fresh installs, since it iterates App.spec.accountPrivileges.

Added field definition (id 8101, version 3.0) and its member entry in
wired.account.privileges, matching the pattern used for
wired.account.message.send_offline_messages (id 8087). The raw-SQL
migration backfill for existing installs was already correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap hardcoded strings in Tabs.swift with L() calls and add keys to
en/de/fr Localizable.strings:

- Section headers: "System Data Directory", "Install Mode", "Versions"
- Picker/Toggle: "Mode", "Start at Boot"
- Picker options: LaunchAgent/LaunchDaemon labels
- Daemon status: "Running (daemon)", "Stopped (daemon)"
- Daemon User / Group field labels, Save button
- Migration description and button
- FDA/external-volume warning panel (all four strings + Re-check, Restart Daemon)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the external-volume/FDA check fails, show "Open Full Disk Access…"
button that opens Systemeinstellungen → Datenschutz & Sicherheit →
Festplattenvollzugriff directly (macOS 12 and 13+ URLs handled).
Add fda.open_settings key to en/de/fr Localizable.strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
macOS creates an invisible "Icon\r" file (Icon + 0x0D carriage return)
in any folder with a custom Finder icon. The existing filter only skips
dotfiles, so this file appeared in Wired file listings and could not be
deleted (path operations choke on the embedded control character).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Width 44 was too narrow for "Gruppe" (de) — last character wrapped to
a second line. Increased to 52 to fit the longest localized label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add isPolling guard so concurrent pollState() calls are dropped
  instead of queuing on the MainActor (was: 196%+ CPU spiral)
- Run pkill -u _wired before dscl . -delete to avoid dscl hanging
  when the daemon user has an active distnoted session
- Track daemon user/group creation in UserDefaults so deactivate only
  deletes accounts that this app created
- Add launchctl bootout of LaunchAgent before starting the daemon to
  prevent port-binding races
- Remove ad-hoc codesign call that was overwriting the Developer ID
  signature written by release.sh --deep
- Check return value of BiometricCredentialStore.save() before
  setting hasTouchIDCredential

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add en/fr/de translations for all hardcoded strings in
switchInstallMode, startDaemon, stopDaemon, and deactivateLaunchDaemon.
Also fix malformed ASCII quote in de.lproj that caused NSDictionary
to return nil, silently falling back to English for all German strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
appendLog() was calling refreshDashboard() on every log line, which
triggered activeServerPIDs() → runProcess(pgrep) → waitUntilExit on
the MainActor, blocking the UI on each log entry during startup.

Also replace the O(n²) string-split pattern (re-splitting the full
logsText on every append) with a dedicated logLineBuffer: [String]
that is appended to incrementally. refreshLogText() now seeds the
buffer so both code paths stay in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eatedly

IndexController.performFullRebuild() called itself synchronously from its
own defer block when pendingRebuild was set by the catch handler. On every
write failure the recursion depth grew by one until SIGBUS at ~400 frames.

Replace the direct recursive call with indexQueue.async so each retry runs
on a fresh stack after the previous call has fully returned.

Also add || true to the launchctl bootout in deactivateLaunchDaemon so the
&&-chain does not abort before chown when the service is already unloaded,
and insert a 1-second sleep before chown so a slow-exiting daemon process
can finish its SQLite WAL checkpoint before ownership is transferred back.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The port check button was silently contacting api.ipify.org and
check-host.net without user confirmation. Restore the per-click
consent alert that was lost when the consent branch was never merged.

- Button shows a confirmation alert (EN/DE/FR) explaining that the
  external IP will be shared with api.ipify.org and check-host.net
- Remove auto-check on app startup — portStatus now starts as .idle
- Add PortStatus.idle case with "Not checked" label (all 3 locales)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el translation

Move .alert from inner HStack (inside GroupBox) to outermost SettingsScrollPane
so macOS SwiftUI reliably presents it. Add missing PortStatus.idle case to
color(for:) switch. Add common.cancel key (Cancel/Abbrechen/Annuler) to all
three locale files — was missing, causing the raw key to appear as button label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setAdminPassword only cleared the password on success; createAdminUser
never cleared it. Use `defer` so the @published property is wiped on
every exit path (early return, success, catch). Limits the lifetime of
the cleartext admin password held in the ViewModel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…m startup"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sion)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows a v3.1 peer to interoperate with a v3.0 peer (and vice versa) by
loosening strict version checks, tolerating unknown messages and
length-prefixed unknown fields on receive, and turning the
compatibility_check exchange into a non-blocking spec diff that drives
sender-side feature gating.

- ProtocolVersion: numeric semver-ish parser (so 3.10 > 3.2)
- P7Spec: same-major compatibility, public init?(withData:/withString:)
- P7Socket: align hardcoded "3.0" with spec.protocolVersion, hard-fail
  only on major mismatch, trigger compatibility_check on any minor
  difference, parse the peer's spec into a CompatibilityDiff, expose
  remoteSpec / negotiatedProtocolVersion / peerKnows() helpers, and
  filter outbound messages/fields the peer doesn't know
- P7Message: bin(omittingFieldIDs:) for the sender filter, decoder no
  longer desyncs on unknown field IDs (records them and aborts cleanly),
  unknown message IDs decode known fields best-effort
- CompatibilityTests: 10 tests covering version parsing, diff
  computation, receiver tolerance, sender filter
- COMPATIBILITY.md: full evolution policy + checklists for adding
  fields/messages; CONTRIBUTING/SECURITY/README updated to point at it

Closes nark#87

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bumps the protocol to 3.2 and introduces a chat-reactions surface that
mirrors the board-reactions API. Public-chat broadcasts now carry a
server-assigned wired.chat.message.id (length-prefixed UUID string) so
clients have a stable handle for reactions, future edits, and
optimistic-UI correlation. Pre-3.2 receivers silently skip the new
field via the receiver-tolerance machinery introduced in nark#87, and the
new reaction messages are filtered out wholesale by the per-session
compatibility diff — a 3.2 client can therefore talk to a 3.1 server
(and vice-versa) without any surface-level breakage.

Reactions are kept in an in-memory ring buffer per public chat
(default 500 messages); when a message scrolls off the buffer its
reactions are dropped, consistent with the "chat is a stream" model.
Reacting on an unknown id surfaces wired.error.invalid_message.

Closes nark#90.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
testConnectAndAcceptHandshakeSucceedsWithNoCrypto pinned the negotiated
remote version string to "3.1"; bumping the protocol to 3.2 broke that
expectation. The handshake itself still works — the spec is wire-compat
with 3.1 — only the asserted version string needs updating.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CompatibilityTests and P7SpecMetadataTests reference TestResources.specURL
which resolves wired.xml via #filePath — robust across macOS xctest and
Linux SPM. The file was referenced in tests merged to main but the file
itself was not included, breaking CI since 2026-05-09.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants