Show published file-sharing protocols and disk health detail - #8
Open
JYPochez wants to merge 45 commits into
Open
Show published file-sharing protocols and disk health detail#8JYPochez wants to merge 45 commits into
JYPochez wants to merge 45 commits into
Conversation
SwiftPM has no .app product type, so until now the app could only be launched
from a terminal via run.sh. Add two equivalent build paths that each produce a
real, self-contained "AirPort Utility.app":
./make-app.sh [--sign] [--notarize] [--zip] shell script, no Xcode needed
open AirPortUtility.xcodeproj scheme "AirPort Utility App"
Both build a universal binary (arm64 + x86_64) and embed the Python backend so
the bundle runs without a source checkout beside it.
Several bundling constraints drove the layout:
* The backend lives in Contents/Resources, not Contents/MacOS. codesign treats
everything in Contents/MacOS as code and fails verification on non-Mach-O
files there ("code object is not signed at all"). This is the only change
that touches shared source: defaultRepoPath() now also resolves the backend
through Bundle.main.resourceURL.
* Info.plist declares NSLocalNetworkUsageDescription and NSBonjourServices. On
macOS 15+ a bundled app is its own TCC principal, and without these Bonjour
discovery silently returns nothing and ACP connections to port 5009 fail.
Running from a terminal masks this, because there the permission belongs to
the terminal.
* LSEnvironment sets PATH, because a Finder-launched app inherits a minimal one
and "#!/usr/bin/env python3" would resolve to /usr/bin/python3, the Command
Line Tools stub (3.9.6). The backend's suite does pass there, but a real
interpreter is preferred when installed.
* LSEnvironment also sets PYTHONDONTWRITEBYTECODE, since Python would otherwise
write __pycache__ beside the bundled backend and invalidate the signature.
* The entitlements file is intentionally empty: the app is not sandboxed, and
Hardened Runtime comes from `codesign --options runtime`. Note that AMFI
rejects XML comments inside an entitlements plist, so the rationale lives in
Packaging/README.md.
Package.swift gains a library product for AirPortUtilityCore so the Xcode
target can link it as a local package product instead of duplicating the
source list.
Tagging v* runs .github/workflows/release.yml, which builds, signs, notarizes,
staples, verifies and attaches the zip to a GitHub release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Adds localization infrastructure plus a first vertical slice: the menu bar, the pane tabs, the Base Station pane and the initial status string. Remaining panes keep working in English and can be migrated incrementally. Keys are the English source strings, so untranslated text falls back to readable English instead of a symbolic key, and the 680 existing tests -- many of which assert on English UI strings -- pass unchanged. Notes on the design: * Language resolution uses Locale.preferredLanguages rather than the one-argument Bundle.preferredLocalizations(from:). That variant matches against the *main* bundle's localizations, which are empty for command-line and test builds, so it silently pins every lookup to English. Verified across en/fr/de, including fr-CA correctly falling back to fr. * Classic .lproj/Localizable.strings rather than a String Catalog: SwiftPM copies .xcstrings into the resource bundle without compiling it, so a catalog would work under Xcode but silently fall back to English via make-app.sh, run.sh and swift test. * Protocol text is deliberately excluded. ACP keys, backend flags and persisted raw values are wire format, not display strings. Pane.rawValue stays English because it is Codable-persisted, used for snapshot file names and used to build accessibility identifiers; Pane.displayName carries the translation. LocalizationTests asserts no table contains an ACP-shaped key or a flag. * Two German strings are shortened from their literal translations. The window is a fixed 800x504 and the literal forms clipped: "Konfiguration ueber Ethernet-WAN-Anschluss erlauben" lost its final character. Rendering each language is the only reliable check for this; the tests cannot catch it. AirPortLocalization.text(_:) is public so the menu bar in the app target can reach the table; PublicAPISurfaceTests is updated for the two new symbols. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Extends the localization slice to the remaining configuration panes and sheets, taking the translations from Apple's own AirPort Utility where it covers the same UI (/System/Applications/Utilities/AirPort Utility.app). Its tables live in compiled NIBArchive files rather than .strings, so the wording was extracted from the nibs and cross-checked term by term. That correction mattered: * German used "Kennwort" throughout; Apple uses "Passwort" (20 occurrences changed). Apple's wording is also consistently shorter, which resolves the fixed-width overflow rather than papering over it -- "Konfiguration ueber WAN zulassen" replaces the clipped "Konfiguration ueber Ethernet-WAN- Anschluss erlauben". * French adopts "Confirmer le mot de passe" and "Conserver dans mon trousseau"; Spanish and Italian likewise follow Apple's own forms. Two protocol hazards were caught by inspection before wrapping: * AdvancedPaneSection had UI labels as enum raw values, which is both a compile error when wrapped and the same persistence hazard as Pane. It now keeps English raw values and carries a displayName. * "Off" and "Disk" appear both as display text and as data -- a networkName sentinel and an accessibility-identifier comparison respectively. They are translated but wired by hand, never swept. Language resolution is pinned to the development language under XCTest. Many tests assert on English UI strings, and this machine runs in French, so the same suite would otherwise pass in CI and fail locally. The identical-to-English check is now per language rather than global: a term that is genuinely identical in German may still be a real word in French, and a global allowlist would stop catching a lazy French entry. A companion test flags allowlist entries that have since been translated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Region names now resolve via Locale rather than the .strings tables. Hand translating 172 country names into four languages would be redundant and error-prone when Foundation already carries authoritative names for every one of them. WirelessRegionOption.code stays the ACP wire index and .name stays English: the English name is the lookup key and the fallback. 154 of the 172 names match Foundation's current English directly; the remaining 18 are spelled differently here (Czech Republic, Ivory Coast, Turkey, ...) and carry an explicit alias. A test asserts the whole list maps, since an unmapped entry would silently fall back to English in every language. Foundation-provided text now follows AirPortLocalization.locale rather than Locale.current, so it obeys the same XCTest pinning as the tables -- otherwise a test would see English labels beside French country names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
The pane widths were measured against English labels and pinned exactly, so
longer translations were cut off: the Internet pane's DHCP button rendered
"Rinnova DHCP asseg..." in Italian and "Renovar la asignació..." in Spanish.
InternetPaneButton pinned its width with an Auto Layout
widthAnchor.constraint(equalToConstant:), and AirPortButtonStyle with an exact
.frame(width:). Both now express a minimum instead. English is unaffected --
its intrinsic width is smaller than the minimum, so the measured width still
wins, verified pixel-identical against a render taken before the change.
Spanish and Italian also adopt Apple's own wording for that button ("Nueva
concesión DHCP", "Rinnova DHCP assegnato"); French and German already matched
Apple exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Adds 103 keys covering DeviceStatusMessage, the topology view and popovers, AirPortServices operation titles and validation messages, and the setup assistant. Excluded by inspection rather than by pattern: ACP problem codes (ArcI, DubN, nDNS, ...), profile paths (settings.sySt.problems), device artwork file names, mock fixtures (Jack's Network, time-capsule.local), the keychain account prefix, and 'time capsule', which is matched with .contains() rather than displayed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Adds 127 keys covering the validation and status text in AirportAppModel*,
which is the bulk of what a user sees while actually operating a device.
Most are formulaic ("X cannot be empty.", "X must be an IPv4 address.",
"X passwords do not match."), so they are generated from templates over a
translated field-name map rather than written out one by one, which keeps the
phrasing consistent across 4 languages.
Two bugs in that generation, both caught before commit:
* Spanish and Italian produced a doubled article -- "El campo el nombre del
altavoz AirPlay no puede estar vacio" -- because the field phrases already
carry one. They now agree in gender instead ("El nombre del altavoz AirPlay
no puede estar vacio", "La contrasena del disco no puede estar vacia").
French and German need no such handling: "vide" and "leer" are invariable.
* Two keys appeared in both batches and were wrapped twice. Collapsing the
doubled localized( prefix left the extra closing paren behind and broke the
build. The helper now refuses to wrap a literal that is already wrapped.
Only the 262 non-UI strings in these files remain untouched: ACP keys,
dictionary keys, identifiers and test-environment probes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
The Cancel and Update buttons in the configuration sheet footer stayed English
because PaneChrome was never swept -- only its pane titles had been touched.
A completeness pass over every source file found the rest: the command-log
placeholder, the dry-run message, disk-inventory and firmware error text, the
router-mode and modem idle option labels, and the window title.
Also fixes a bug introduced when the default status was localized. The sheet
footer decided visibility with
status != "Not connected"
against the English literal, so in French the comparison was always true and
the footer showed a status it was meant to suppress. It now compares against
the same localized value. The neighbouring "Connected to ..." and "Ready to
connect to ..." prefixes are deliberately left English: they interpolate the
host, so they were never localized, and changing only the comparison would
break them. A comment records that the two must move together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Replaces the placeholder io.github.jackhumphries.airport-utility in the three places it was defined: make-app.sh, the Xcode target, and the release workflow fallback. BUNDLE_ID still overrides it for one-off builds. Note that the identifier is what macOS ties Local Network (TCC) approval and keychain access to, so the first launch after this change prompts again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Every device rendered amber in a non-English UI. The topology decides a
device's status colour by comparing display text:
status == localized("Working normally") ? green : amber
but several producers of that text still returned the English literal, so the
comparison never matched once the UI was localized. Fixed at every producer:
the BaseStationState default and its Codable decode fallback, the "Restoring"
and "Restarting" states in deviceStatusText, and the mock backend's status
values including its default branch.
Reverted one comparison instead: TopologyPopovers matched model.status against
localized("Connected"), but that status is "Connected to <host>", which
interpolates the host and is therefore still English. Localizing only the
comparison broke it. It now matches English again, with a comment saying the
two must move together.
Adds a regression test asserting BaseStationState().statusText equals
DeviceStatusMessage.text(problemCodes: []). Both mean "no problems" and the
topology compares them, so they must be the same string in every language.
The underlying hazard is that status is display text used as a state token.
The comparisons are now consistent, but a status enum would remove the class
of bug entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
…room "Mettre à jour" rendered as "Mettre à..." in the sheet footer. Seven controls across six files pinned their width with an Auto Layout constraint measured against the English label, so any longer translation was truncated. Each constraint is now equalToConstant: max(designWidth, intrinsicContentSize) -- still exact, so the control never expands to fill, but never narrower than its label needs. A plain greaterThanOrEqualToConstant was tried first and rejected: removing the upper bound let SwiftUI's proposed width win, so the buttons expanded to fill their row and the English Disks pane visibly changed, with Erase Disk and Archive Disk growing until they nearly touched. Verified by pixel-diffing all 17 rendered panes against a baseline taken before the change: 16 are identical, and firmware.png shifts about two points because one English label was fractionally wider than its constant, which is the bug being fixed rather than a regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Three problems, all visible in the Wireless Options sheet.
Cancel and Save were still English there. A sweep for literals that already
have a translation but were never wrapped found 28 across seven files: the
sheet buttons, the erase-method and modem-idle option labels, several topology
popover fields, and a few stragglers. Enum raw values, the "Off" sentinel,
Bonjour model names and the firmware download directory are excluded, since
those are data rather than display text.
The option labels truncated ("Débit multica...", "Puissance d'é..."). They are
right-aligned and end at the control column, so the label frames now extend
leftward to the sheet margin -- English is unaffected because the text is
right-aligned and its trailing edge does not move. "WPA Group Key Timeout" is
long enough that it also needed shorter wording in all four languages.
Widening the buttons then made "Abbrechen" overlap "Sichern": the pair was
placed at fixed x offsets, so a wider Cancel grew rightward into Save. All
three options sheets now right-anchor the pair in an HStack, so longer labels
grow leftward. The trailing edge and 12pt gap reproduce the English layout
exactly.
Verified by pixel-diffing all 17 panes against a baseline: only firmware.png
differs, by the ~2pt already accounted for in the previous commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
VNS.airport.utility was hardcoded in make-app.sh, the Xcode target and the release workflow. That is fork branding and does not belong upstream, but it still has to reach the compiled app. The identifier now lives in Packaging/Base.xcconfig with a neutral default, ending in an optional `#include? "Local.xcconfig"`. Packaging/Local.xcconfig is gitignored, so a fork sets its own identifier without modifying a tracked file, and a clone without that file builds with the neutral default rather than failing. The target's buildSettings no longer define PRODUCT_BUNDLE_IDENTIFIER, since a target-level setting would override the xcconfig. make-app.sh reads the same Local.xcconfig, so the script and Xcode agree; BUNDLE_ID in the environment still overrides both. Verified: with Local.xcconfig present both configurations resolve to VNS.airport.utility and the signed bundle carries it; with the file removed, Xcode resolves the neutral default and the build still succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Four things were still English or truncated in a French UI. The DHCP lease unit menu (second/minute/hour/day/week) and the syslog level menu (0 - Emergency ... 7 - Debug) showed English. Only the label: argument is wrapped; value: and level: are wire values and stay as they are. The erase and archive confirmation titles were never localized because each embedded a literal \n, which kept them out of the extraction sweep. They are localized now without the line break -- where English happens to wrap is not where a translation should. Body text in both disk sheets truncated rather than wrapping: the frames were sized to the height of one or two English lines, so a longer translation was cut instead of flowing. Each frame now grows into the space before the next control. The Advanced pane's segmented tabs overflowed a fixed 330pt control shared by three segments. Widening it would change the English layout, so the two long tab labels take shorter forms per language instead. Both disk sheets also right-anchor their button pairs, as the options sheets already do. Note: the snapshot renderer does not cover the erase and archive sheets, so those two were verified by reading the layout rather than by rendering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
…ncation The popover's Edit button read "Édition" in French. That is the Edit *menu*, not the verb. With the English source as the lookup key there was no way to tell the two apart, so AirPortLocalization.text now takes an optional context that is tried as "context.key" before the plain key. The menu bar asks for "menu.Edit" and keeps "Édition"; the bare key is the verb used on buttons and becomes "Modifier". Two other Edit buttons, in the network port-mapping table, were reading the menu translation for the same reason and are fixed by it. The "status" and "network" row labels were never wrapped, so they stayed English beside translated siblings. Three truncations: the Edit button was pinned to the 47pt English width and now sizes to its label; "LAN IP address" overflowed the 108pt label column and takes a shorter form per language; and the value column was boxed at 152pt, which cut "Fonctionne normalement". The popover is 20pt wider so the value column gets 172pt -- the label column origin and the value origin are unchanged, so no English text moves, only the trailing edge. Removing the button's fixed width alone made it expand to fill the row; fixedSize() pins it to its intrinsic width instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Package.swift declares swift-tools-version 6.0, but the project did not build
on Swift 6.0 -- only on the newer toolchain most contributors happen to have.
CI surfaced it on an Xcode 16 runner:
error: call can throw, but it is not marked with 'try'
accounts += selectedTopologyDeviceIdentifiers.compactMap(Self.passwordStoreAccount)
passwordStoreAccount is a plain non-throwing "-> String?". The problem is that
passing it as a *function value* to the nonisolated rethrows compactMap crosses
an actor boundary, since it inherits @mainactor from AirportAppModel. Swift 6.0
reports that as the argument being able to throw; 6.2 accepts it.
Calling it from inside a closure keeps the call in the isolated context, so no
function value crosses the boundary. Behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
The compiled app is a release asset, not a file in the tree, so nothing on the repository front page led to it. GitHub's sidebar callout and its /releases/latest endpoint both ignore pre-releases, and v0.1.0 is flagged as one, so neither surfaced the download either. Adds a Download section near the top of the README linking to the v0.1.0 tag directly, with the two things that otherwise look like bugs on first launch: the Local Network prompt and the Python requirement. Fork-specific, so it lives on main only and is deliberately not part of either upstream pull request.
This reverts commit d4fb703.
The release workflow pinned Xcode 16, whose Swift 6.0 rejects concurrency patterns this project uses -- passing actor-isolated methods as function values to nonisolated generics, in AirPortCommandRunner, AirPortServices, TopologyView and AirportAppModelPasswords. None of that is specific to this branch; the project simply needs a newer toolchain than the pin allowed. Selects the highest-numbered Xcode present instead, and logs what was available so a future mismatch is diagnosable from the run alone. This supersedes the previous commit, which patched one of those call sites. That was treating a symptom: the other three remained, so the project still did not build on Swift 6.0, and the change was unrelated churn in shared code. Reverted.
The file holds a fork's own bundle identifier and signing team and is meant to stay untracked. The ignore rule lived only on a later branch, so on this one it was not ignored at all and got committed. Adds the rule here, where Packaging/ is introduced, so it applies to every branch built on this one.
# Conflicts: # .gitignore
The setup assistant still showed English headings ("What do you want to do with
this AirPort Time Capsule?") because those strings interpolate the device model
name, so the English source could not be the lookup key -- the key would change
with the value. Adds localizedFormat, which looks up a format ("What do you want
to do with this %@?") and fills it in, letting a translation move the
placeholder where its grammar needs it. Applied to the setup assistant and to
the connection status messages.
The sheet footer hid connection messages by matching the English prefix
"Connected". Localizing those messages would have silently broken that, so it
now derives the prefix from the same format string it was built from, which
holds in any language. This replaces the note left earlier warning that the two
had to move together.
passwordRow derived its accessibility identifier from the label with
localizedCaseInsensitiveContains("Disk"), which stops matching once the label is
translated. The identifier is now passed in by the caller.
Tab widths were constants measured against the English labels, so translated
labels were cramped. Each tab is now sized to its own label, with the English
constants kept as a floor: 19pt is the tightest padding those constants imply,
so no English tab can grow and that layout is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
It named v0.1.0 explicitly and went stale as soon as v0.1.1 shipped. The list page always shows the newest build; /releases/latest is not usable here because it ignores pre-releases, which both of these are.
Tagging a release in a repository without the signing secrets failed at the certificate import, leaving a red run on every tag. That is the state any fork starts in, including this one, so the workflow reported a problem where there was none. The credential-dependent steps are now skipped when MACOS_CERTIFICATE is unset: the run builds, tests and stops, and emits a notice saying signing and publishing were skipped and why. With the secrets configured it behaves exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Two additions to the disk and topology surfaces, both reading data the device already publishes rather than introducing new commands. The device popover gains a "file sharing" row listing the protocols the base station advertises over Bonjour. The browser now watches _afpovertcp._tcp and _smb._tcp alongside _airport._tcp, matching those services to a device by name, which is how a base station publishes its shares. Those services are recorded as evidence about a device rather than treated as devices themselves, or every share would appear as its own base station. The row is omitted when nothing is advertised. Note what this cannot say: Bonjour advertises that SMB is present, never which dialect. SMB1 vs SMB2 vs SMB3 is negotiated per connection, so reporting a version would mean either implementing an SMB client or inferring it from the firmware version and presenting a guess as fact. The Disks pane gains a S.M.A.R.T. check. The status is already carried in the disk inventory the pane reads, so this surfaces it rather than starting anything: there is no ACP command to initiate a self-test, and the button says "check" only in the sense of reading what the device reports. The inventory reports SMART once per physical disk, so the parser carries it down to each partition, with a partition's own value taking precedence. Two tests cover both directions. The smartStatus field name was identified from the pepperstm fork, which surfaces the same value; the implementation here is separate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Two additions to the disk and topology surfaces, both reading data the device already publishes rather than introducing new commands. The device popover gains a "file sharing" row listing the protocols the base station advertises over Bonjour. The browser now watches _afpovertcp._tcp and _smb._tcp alongside _airport._tcp, matching those services to a device by name, which is how a base station publishes its shares. Those services are recorded as evidence about a device rather than treated as devices themselves, or every share would appear as its own base station. The row is omitted when nothing is advertised. Note what this cannot say: Bonjour advertises that SMB is present, never which dialect. SMB1 vs SMB2 vs SMB3 is negotiated per connection, so reporting a version would mean either implementing an SMB client or inferring it from the firmware version and presenting a guess as fact. The Disks pane gains a S.M.A.R.T. check. The status is already carried in the disk inventory the pane reads, so this surfaces it rather than starting anything: there is no ACP command to initiate a self-test, and the button says "check" only in the sense of reading what the device reports. The inventory reports SMART once per physical disk, so the parser carries it down to each partition, with a partition's own value taking precedence. Two tests cover both directions. The smartStatus field name was identified from the pepperstm fork, which surfaces the same value; the implementation here is separate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
…eneration Three changes, one of them a fix to the previous commit. **The file-sharing row never appeared.** Browsing _afpovertcp._tcp and _smb._tcp was not enough: macOS only permits browsing the Bonjour service types an app declares in NSBonjourServices, and Info.plist listed only _airport._tcp, so the browse returned nothing and the row was always omitted. The two types are now declared. Confirmed against real hardware with dns-sd, which shows six Time Capsules advertising _smb._tcp and five of them _afpovertcp._tcp. **SMART moved from a button to the volume it describes.** A button implied an action; the status simply arrives with the disk inventory. It now sits on the row's second line beside free space, which also localizes that line -- "497.85 GB Free" was still English, having been skipped as interpolated text. The reported value passes through the localization table, so a known term is translated and anything unfamiliar shows verbatim. The disk name was not a candidate for this: it is an editable field. **The Base Station pane names the hardware generation.** The device reports a product ID and no generation, so the mapping comes from jamesyc/TimeCapsuleSMB, which reads the same syAP field. Generations are numbered per product line, so a Time Capsule and an Extreme can share a number without being the same hardware. An unknown product ID shows no row rather than a guess. The labels are written out per language because ordinals carry gender and abbreviation rules a formatter gets wrong -- "1re génération", not "1er génération". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
# Conflicts: # Sources/AirPortUtilityCore/Resources/de.lproj/Localizable.strings # Sources/AirPortUtilityCore/Resources/en.lproj/Localizable.strings # Sources/AirPortUtilityCore/Resources/es.lproj/Localizable.strings # Sources/AirPortUtilityCore/Resources/fr.lproj/Localizable.strings # Sources/AirPortUtilityCore/Resources/it.lproj/Localizable.strings
A conflicted merge in a .strings file does not break the build: Swift never parses these, so the markers are copied into the app bundle verbatim and everything appears to succeed. That happened during this branch's merge and produced a signed app carrying conflict markers in all five tables.
The merge kept both the button from the earlier commit and the inline status that replaced it, since they sit in different hunks and git combined them additively. The button, its model action and its now-unused strings are gone; the status stays on the volume row. Also shortens the file-sharing label per language. The popover label column is a fixed 108pt, sized for "serial number", and "partage de fichiers" overflowed it.
The window widens as base stations are discovered. Centred, it expands in both directions, so on a network with several devices it ends up partly off-screen. Anchored near the top-left it grows right and down into free space. Positioned against the screen's visibleFrame, so it clears the menu bar and the Dock, and falls back to centring when no screen is available.
The row label was a bare literal, invisible in French and German where the word is identical but wrong in Spanish and Italian.
The button survived an earlier merge, which combined it additively with the inline status meant to replace it. Removed along with its model action. Free space is missing for real hardware while Apple's own utility shows it, so the value is reported somewhere this parser does not look. Capacity now falls back to the physical disk the same way SMART already does: some devices report size and sizeFree once per disk rather than per partition. A partition's own values still win. This is a plausible cause, not a confirmed one -- it needs checking against a real inventory. Tests cover both directions of the fallback, and caught that maSt reports megabytes rather than bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
The Disks pane showed no free space against real hardware while Apple's own
utility showed it. A MaSt read explains why: the device never sends bare
numbers. Every integer arrives wrapped,
"sizeFree": {"decimal": "623863", "type": "integer", "width": 4}
and the decoder handled only JSON numbers and strings, so every size decoded to
nil and the subtitle was omitted. It now reads the decimal field.
The earlier guess -- that capacity was reported per physical disk and needed
inheriting -- was wrong: sizeFree sits on the partition exactly where the parser
looked. The inheritance is kept because SMART genuinely is reported per disk,
and the fallback is harmless where a value is already present.
Adds a test built from a real device's response, with identifiers anonymised and
the structure verbatim, which also documents that MaSt reports megabytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
The row now carries three lines: the volume name, then the physical disk's vendor and firmware revision, then "used / total" with the SMART status. Both detail lines are 10pt — at 11pt the German and French capacity lines clipped the SMART status off the end, and the longer "502,15 GB used" phrasing clipped it even at 10pt, hence the compact "used / total" form. Used size comes from the partition when the device reports it and is otherwise derived from total minus free. Vendor and revision are only reported on the physical disk, so InheritedDiskValues threads them down to the partitions alongside the SMART status and capacity. The mock's disk inventory was a second, hand-written set of DiskRecord literals that had drifted from the mock MaSt payload; it now decodes that payload so the two cannot disagree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
"1.34 TB / 2 TB" leaves the reader to guess which half is which. The first number is now labelled — "1,34 To utilisés / 2 To" — and the total is left implicit, since it is the only thing the second number can be. The label does not always fit: a full-precision pair with the Spanish wording is 244pt against 205pt of row, and even the unlabelled form overran it at 206pt, so the fit was luck of the numbers rather than anything the layout guaranteed. Both detail lines now scale down to 8pt instead of truncating, which keeps the SMART status — the part worth reading — on screen. DiskInventoryRow becomes internal so its two line builders can be tested; the new cases cover the labelling, deriving used from free, and dropping a line entirely when the device reports nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Surfaces two things the base station already reports but the app never showed: which
file-sharing protocols a device publishes, and the health and capacity of its disks.
Published file-sharing protocols. The Bonjour browser now also watches
_afpovertcp._tcpand_smb._tcpand records, per device name, which of them itadvertises. The device popover shows the result ("AFP, SMB"), so you can tell at a
glance whether a Time Capsule is actually publishing its disk.
NSBonjourServicesinthe bundle's Info.plist declares the two extra service types — without that entry
macOS's Local Network privacy gate silently returns no results.
Disk detail. Each partition row now carries three lines: the volume name, the
physical disk's vendor and firmware revision, and "used / total" with the reported
SMART status:
The used figure is labelled and the total left implicit: "1.34 TB / 2 TB" alone never
says which half is which, and there is not room for both words.
Vendor, revision, SMART status and capacity are only reported on the physical disk, so
InheritedDiskValuesthreads them down to the partitions underneath it. Used size comesfrom the partition when the device reports it and is otherwise derived from total minus
free. Note that
maStreports megabytes, and that its integers arrive wrapped as{"decimal": "…", "type": "integer", "width": n}rather than as bare JSON numbers —DiskInventoryParserhandles both shapes, with a regression test built from a realdevice payload.
There is no SMART test: the status shown is the one the device already keeps, so the
row needs no button and costs no extra round trip.
Notes
The row has about 205pt of text width, which a full-precision capacity pair overruns
even unlabelled ("502,15 GB / 931,32 GB · SMART: verificado" is 206pt), so shrinking is
what keeps the SMART status on screen in the worst cases. Ordinary values render at the
full 10pt.
DiskRecordliterals that had drifted from the mock
MaStpayload; it now decodes that payload sothe two cannot disagree.
Testing
swift test— 702 tests, 0 failuresAIRPORT_UTILITY_MOCK=1snapshot renders in all five languages