| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Bumps the app to 5.0.0 now that all four v5.0.0 workstreams have landed:
- AppVersion.current 4.9.0 -> 5.0.0
- MARKETING_VERSION 4.9.0 -> 5.0.0 across all targets (app, widget, share
extension, and both test bundles)
- CURRENT_PROJECT_VERSION 44 -> 45 across all targets
- RELEASE_ROADMAP.md: v5.0.0 marked shipped, "Current version" -> v5.0.0
The three version sources are kept in lockstep, matching the v4.4.1 alignment
policy. App Store archive/submit remains a manual step outside the repo.
|
| |
|
|
|
|
|
|
|
|
|
| |
All four v5.0.0 workstreams have landed on main: the deterministic-core test net
(#43), the Local API v1 contract (#45), the versioned store-migration policy
(#46), and the god-file decomposition (#47-#53, DomainViewModel 4864->4170 and
ContentView 3881->1625). Rewrites the v5.0.0 section to record what shipped per
item with PR references, notes the inspection-core split is deferred as a design
change rather than a mechanical move, and lists cutting the release (version
bump + submission) as the only remaining step. Refreshes the now-stale
cross-cutting note (the "no XCTest target" gap was closed first).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Seventh slice of the god-file decomposition; second ContentView.swift slice.
- Moves the nine result detail section views into ResultSectionViews.swift:
DomainSectionView, OwnershipSectionView, IntelligenceSectionView,
SubdomainsSectionView, DNSSectionView, WebSectionView, EmailSectionView,
NetworkSectionView, and PortRowsView. Pure move.
- Promotes the one-line appLoadingStyle() View helper private -> internal: the
moved section views and the staying LoadingCardView both use it. That is the
only non-deletion edit to ContentView.swift.
The shared primitives (CardView, SectionTitleView, LabeledValueRow,
ResultColors) stay in ContentView.swift and are reached cross-file.
ContentView.swift: 2738 -> 1626 lines (3881 at the start of the ContentView
pivot; -2255 total). App builds clean; unit suite 58/58. No project.pbxproj
change.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pivots the god-file decomposition to ContentView.swift (the other ~3.9k-line
file). Unlike DomainViewModel, this is a set of independent SwiftUI View structs,
so it splits with no shared-state entanglement.
- Moves the Settings surface into SettingsViews.swift: SettingsView (the tab
root, presented from RootTabView) plus its nine file-private section screens
(Display, History & Network, iCloud Sync, Local API, Monitoring, Import &
Export, Data Management, About) and the DataImportPreviewSheet. Pure move.
- The private section screens stay file-private together in the new file, reached
only through SettingsView's navigation links. Zero visibility changes were
needed: the block references nothing file-private to ContentView.swift, so the
ContentView diff is pure deletion.
ContentView.swift: 3881 -> 2739 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
step 4, 5/n)
Fifth slice of the DomainViewModel decomposition.
- Moves the history query/compare surface into DomainViewModel+History.swift:
per-domain snapshot reads (historyEntries/historyEntry/previousHistoryEntry),
timeline grouping (timelineEntries/timelineSections), the two-snapshot
selection model (toggle/clear/selectedSnapshots), diff generation and
navigation (generateDiff*/moveTo*DiffChange/currentDiffTargetSectionID), and
the resolver-mismatch notes. Pure move.
- The inspection *pipeline* that produces history — performLookup,
applySnapshot, saveHistoryEntry, persistHistory, snapshot-metadata bookkeeping
— stays on the main type with the inspection code.
This slice needs zero visibility changes: every dependency (the history array,
diff state, latestSnapshot) was already internal. The main-file diff is pure
deletion.
DomainViewModel.swift: 4306 -> 4171 lines (4864 at the start of step 4; -693
total). App builds clean; unit suite 58/58. No project.pbxproj change.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
4, 4/n)
Fourth slice of the DomainViewModel decomposition (off main; the audit/
monitoring/export stack has merged).
- Moves the workflow surface into DomainViewModel+Workflows.swift: workflow
lookup (workflow(withID:)/workflowsContaining), the DomainWorkflow
collaboration checks, the CRUD mutators (create/update/delete/add/remove/move),
runWorkflow/rerunCurrentDomain, and refreshWorkflowList. Pure move. The
TrackedDomain overloads of canEdit/canDelete/collaborationLabel stay on the
main type (they're watchlist collaboration, not workflow).
- Promotes the shared helpers the moved methods reach to internal, all staying
on the main type: persistWorkflows (also called by clearWorkflows),
startBatchLookup (the batch primitive shared with manual/watchlist runs),
normalizedDomain/normalizedDomains, loadWorkflows, and the activeWorkflowRunID/
Name state. The extension carries its own fileprivate String.nilIfEmpty,
matching the per-file pattern already used across the codebase.
DomainViewModel.swift: 4433 -> 4306 lines (4864 at the start of step 4).
App builds clean; unit suite 58/58. No project.pbxproj change.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
3/n)
Third slice of the DomainViewModel decomposition, stacked on the monitoring
split.
- Moves the export/data-portability surface into DomainViewModel+Export.swift:
the single-report/batch/tracked-domain/timeline/workflow exporters, the
full-backup and portable-slice exporters, prepareDataImport/applyDataImport,
and persistCurrentAppSettings. The export-only WorkflowExportPayload struct
moves with them. Pure move, no logic changes.
- The four report-projection helpers the exporters call (currentBatchReports,
reports(for:), timelineReports, workflowReports) are promoted private ->
internal and stay on the main type: they build DomainReports through the
shared report layer (report(for:)/reportBuilder), so they belong with
inspection, not export. Those four visibility drops are the only non-deletion
edits to DomainViewModel.
DomainViewModel.swift: 4601 -> 4434 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
step 4, 2/n)
Second slice of the DomainViewModel decomposition, stacked on the audit split.
- Moves the monitoring configuration surface — notification authorization, the
settings mutators (enabled/scope/interval/adaptive/sensitivity/quiet-hours/
alert filter/alerts/selection), toggleMonitoring, runMonitoringNow, the manual
run, and the per-domain interval/status labels — plus the private
intervalLabel helper into DomainViewModel+Monitoring.swift. Pure move.
- persistMonitoringSettings(...) and sanitizeMonitoringSelection() are promoted
private -> internal: the tracked-domain lifecycle (add/delete/clear) on the
main type calls them too, so they stay put and are now reachable cross-file.
The two visibility drops are the only non-deletion edits to DomainViewModel.
DomainViewModel.swift: 4761 -> 4601 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
First slice of the v5.0.0 god-file decomposition. DomainViewModel.swift is a
single ~4.9k-line class body; this begins splitting it into focused
`DomainViewModel+<Concern>.swift` extensions, one cohesive concern at a time,
with each move behavior-preserving and verified by build + the test net.
- Moves the audit read/CRUD/export surface (audits/auditSession/auditTimeline,
updateAuditStatus/Notes, toggleAuditChecklistItem, add/update/remove
AuditFinding, exportAuditData) into DomainViewModel+Audit.swift as an
extension. Pure move — no logic changes.
- `startAudit(for:)` intentionally stays on the main type: it drives a live
inspection to seed the session, so it belongs with the inspection pipeline
until that is extracted. `persistAuditSessions()` is promoted from private to
internal so both files can call it (its only cross-file dependency).
No project.pbxproj change is needed — DomainDig/ is a file-system-synchronized
group, so the new file is picked up automatically.
DomainViewModel.swift: 4864 -> 4761 lines. App builds clean; unit suite 58/58.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Third v5.0.0 roadmap item: define and implement a migration policy for the
on-device persisted store (tracked domains, history/snapshots, audits,
workflows, monitoring, settings), so data upgrades cleanly across app versions
instead of relying on a one-shot marker.
- DataMigrationService is reworked from a single boolean marker
(`data.migrations.v3_4_0`) into a versioned runner keyed by an integer store
schema version (`data.storeSchemaVersion`). It runs each step once in
ascending order up to `currentStoreSchemaVersion`, stamping the version as it
goes. Adding a future migration is now a `case N:` plus a version bump.
Policy guarantees, all covered by tests:
- Forward-only and idempotent; every step must be safe on an empty/older store.
- Never downgrades: a store written by a newer build (higher version) is left
byte-for-byte untouched.
- Pre-versioning installs are handled: a set legacy boolean marker reads as
"already at v1", so the v1 normalization never re-runs for them.
v1 is the existing normalization pass (dedup + drop the legacy `watchedDomains`
key + sanitize monitoring settings), now expressed as migration step 1.
- Docs/data-migration.md documents the persisted surface, the two independent
version lines (store vs. backup export), when to use lenient decoding vs. a
migration step, the runner contract, an "adding a migration" checklist, and
backup-import compatibility. Linked from the README.
- DataMigrationServiceTests: 6 tests over legacy fixtures — fresh-store stamping,
legacy `watchedDomains` migration + key drop, in-place dedup of the stored
blob, idempotence, legacy-marker-as-v1, and the no-downgrade guard. Full unit
suite: 58 passing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Second v5.0.0 roadmap item: make the Local API's public JSON contract explicit,
documented, and regression-locked, so external consumers (Shortcuts, scripts,
integrations) have a stable surface with a defined compatibility promise.
- LocalAPIContract: new single source of truth for the wire-format version
("v1") and the canonical JSON encoder (ISO-8601 dates, sorted keys). Both the
success and error paths in LocalAPIService now route through it, so the format
can't drift between them, and the ad-hoc per-call-site encoders are gone.
- The response envelope and every payload struct are promoted from `private` to
internal so the contract is a first-class, testable part of the module. The
transport/handler internals (request parser, HTTP response, secret store)
stay private.
- Docs/local-api.md documents the base URL/auth, the envelope, the encoding
conventions (notably: absent optionals are omitted, not null), every endpoint
and its payload fields, the error codes, and the semantic-version-style
compatibility policy (additive changes keep v1; renames/removals/type changes
bump the version). Linked from the README.
- LocalAPIContractTests: 16 structure/"golden" tests pinning the envelope shape,
each payload's field names, the enum encodings, and the ISO-8601 date format.
They assert structure, not values, so ordinary behavior changes don't churn
them but a renamed or dropped field fails CI. Full unit suite: 52 passing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Scripts/audit-a11y.sh: replace `[ ... ]` with `[[ ... ]]` in the seven
shell conditionals SonarCloud flagged (shelldre:S7688). The script is bash
(`#!/usr/bin/env bash`), so `[[` is safe and avoids word-splitting/globbing
footguns. The jq `[ .devices | ... ]` filter is untouched — it is not a
shell test.
- AccessibilityAuditHarness.swift: fold the nested `if` in the noise
classifier into a single conditional (swift:S1066); behavior unchanged.
The remaining open SonarCloud issues (S1075 https literals, S115 external-JSON
CodingKeys, S107 parameter counts, S3087 callback nesting) are false positives
or intentional and are being resolved as Won't Fix / Safe in SonarCloud,
consistent with the v4.8.3 static-analysis policy in RELEASE_ROADMAP.md.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Stands up the DomainDigTests unit-test target the v5.0.0 roadmap flags as the
mandatory first move before decomposing the god-files and locking external
contracts. The project had no XCTest unit target — only the DomainDigUITests
accessibility suite.
- New DomainDigTests target (unit_test_bundle), hosted by the app so
@testable import DomainDig links. Mirrors the UITests build settings
(SWIFT_VERSION 5.0 + MainActor default isolation) to avoid the XCTest
override-isolation issue recorded for the Swift 6 targets. Wired into the
shared DomainDig scheme's Test action, so it runs in CI and the pre-push
audit hook automatically (both invoke the whole scheme).
- SnapshotFixture builds the deep LookupSnapshot/DomainReport models through
their real initializer and the DomainReportBuilder, exposing only the fields
the tests vary.
- 36 characterization tests across the four deterministic units the roadmap
names:
- DiffService: change classification, case/whitespace normalization, DNS
record reorder-vs-change, summary phrasing, resolver context note,
certificate-warning thresholds.
- DomainReportBuilder: snapshot -> report field mapping, primary-IP and
DNSSEC derivation, TLS status, partial-snapshot/validation passthrough.
- DomainReportExporter: format dispatch, JSON round-trip (the machine
contract), CSV/markdown/text/PDF structural invariants, timeline.
- DomainDataPortabilityService: CSV round-trip and the merge/dedup semantics
(case-insensitive collapse, OR-merged pin state, min-created/max-updated,
recency sort) via an ephemeral UserDefaults suite.
- build.yml comment updated to reflect the second test target.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Adds a one-tap way to get a working, authenticated request from the Local
API settings page. copyCurlCommand() copies a curl invocation using the
current bound address and token via the Authorization header:
curl -H "Authorization: Bearer <token>" http://127.0.0.1:<port>/portfolio
Keeps the token in a header rather than a query string, so it stays out of
URLs, browser history, and the request-log view (which masks the token).
Guards on an empty token, matching copyToken().
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
requiredLocalEndpoint (#30)
The Local API failed to start with a generic "Could not start Local API on
port 47821" and no actionable cause. Two problems:
1. The NWListener construction catch discarded the caught error and
substituted a generic string, so the real NWError was never surfaced or
logged. Now the underlying error is logged via stateLogger (matching the
.failed branch) and appended to the thrown message.
2. parameters.requiredLocalEndpoint pinned 127.0.0.1:<port> while the same
port was passed as the listener's on: argument. Pinning a required local
endpoint conflicts with the listener's own port assignment; acceptLocalOnly
already constrains binding to the local link. Dropped requiredLocalEndpoint,
keeping acceptLocalOnly + allowLocalEndpointReuse.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
With the entitlement gate removed, enabling iCloud sync surfaced a chain
of CloudKit errors, each rooted in the query-based fetch:
- "Did not find record type: TrackedDomain" — a fresh container has no
schema until the first save, so querying any type failed.
- "SharedDB does not support Zone Wide queries" — the shared database
rejects database-wide queries.
- "field 'recordName' is not marked queryable" — TRUEPREDICATE queries
require a Queryable index on recordName, which auto-created development
schemas do not have.
All three are inherent to reading with CKQuery. Move the user's own
records into a single custom record zone and read every record with
CKFetchRecordZoneChangesOperation, which needs no queryable indexes and
works on a brand-new zone. The shared database is read the same way, one
zone per accepted share. A custom zone is also a prerequisite for CloudKit
sharing, so share root records now live there too.
- Ensure the custom zone exists before every push and fetch (idempotent).
- Save all records — and build tombstone delete IDs — in the custom zone.
- Replace the per-type CKQuery fetch with a per-zone change fetch that
drains truncated responses via the server change token.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
introspection (#29)
iCloud sync always failed with "This build does not have the CloudKit
entitlement required for iCloud sync." on normal Debug/device builds.
`cloudKitContainer()` gated construction on `isEntitlementConfigurationAvailable()`,
which inspected `Info.plist` keys and an `archived-expanded-entitlements.xcent`
file. Entitlements live in the code signature, not `Info.plist`, and the `.xcent`
file only exists in archived/distribution builds — so the check returned false for
a correctly-signed development build, the container was never created, and every
path reported `.missingEntitlement`.
Stop introspecting entitlements. Always construct `CKContainer.default()` and let
CloudKit's own `accountStatus()` drive availability. When the entitlement is
genuinely absent, CloudKit surfaces `.missingEntitlement` / `.badContainer` /
`.permissionFailure`, which `accountStatus()` now maps back to the "no entitlement"
message — preserving that signal without the false negative.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The docs cleanup removed Docs/ACCESSIBILITY_VERIFICATION.md and the
results doc, but the v4.9.0 roadmap entry still linked to the checklist
in two places — dead links, and with issue #21 now closed the roadmap is
the only durable record left.
- Both dangling links removed.
- The deferred list was stale in the other direction too: it listed the
Liquid Glass runtime check as outstanding, but PR #38 ran it on
simulator alongside the classic-chrome floor. It now names only what a
Simulator genuinely cannot do — VoiceOver speech and the rotor, Voice
Control, iPad Full Keyboard Access, Smart Invert — and notes that the
underlying accessibility metadata is asserted in tests even though its
spoken output is not.
- Records the Phase 6 verification work itself (#38), which landed after
the v4.9.0 entry was written: the metadata and screenshot test suites,
the middle-band Dynamic Type sweep, and the enforced iOS 27.0
dynamicType finding it caught.
Verified the suppression that finding produced rather than taking it on
trust: it is scoped to .dynamicType plus an exact allowlist of the five
Settings section titles, all of which are plain `Section("…")` with no
app-applied font, so UIKit owns the scaling. Enforcement on
app-controlled text is intact.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
27.0 fix
Executes the Phase-6 manual runbook against the simulator, converting the
mechanically-checkable parts into permanent coverage and reporting the rest
honestly by tier.
- Fix an enforced `.dynamicType` failure surfaced by `audit-a11y.sh current`
on iOS 27.0: the Settings `Section("Services")` system header (app sets no
font; 18.6 floor and 26.x CI are clean). Narrow, proven `noiseReason`
carve-out scoped to dynamicType on the exact Settings header titles. Delta:
current FAIL -> SUCCEEDED, finding still prints as `[noise: …]`.
- AccessibilityMetadataTests: assert the icon-only control labels and the dense
Watchlist/Batch row label+value contracts (green on 18.6 and 27.0). These
were one-time manual VoiceOver checks; now they gate.
- Middle-band Dynamic Type sweep at AccessibilityL across the seeded screens.
Found no band-exclusive third bug (recorded), retained as regression
insurance for a band that historically shipped two.
- AccessibilityScreenshotTests: best-effort, non-gating capture utility used to
produce the cross-runtime Light/Dark/AXXXL screenshots (simctl appearance
does not propagate headlessly; driven through the in-app picker instead).
- Docs/ACCESSIBILITY_VERIFICATION_RESULTS.md: full pass/fail/not-executable
matrix + 15 screenshots. Notable positive result: Differentiate Without Color
IS verifiable via the global com.apple.Accessibility defaults domain.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
MARKETING_VERSION 4.8.3 -> 4.9.0 and CURRENT_PROJECT_VERSION 43 -> 44
across all targets, with AppVersion.current aligned — the three-way
consistency v4.4.1 established.
The roadmap gains the v4.9.0 entry: the full accessibility pass (#21
phases 0-5 — semantic colours, light mode and the appearance setting,
Dynamic Type reflow and tap targets, VoiceOver, colour independence and
motion/transparency), the audit harness with seeded fixtures and the
engaged enforcement ratchet, the manual verification checklist, and
Swift 6 language mode adoption (#27). Deferred device passes are named
rather than implied.
The v5.0.0 "establish a test target" bullet is rewritten to match
reality: a UI test target now exists with an enforcement gate; what
remains for v5.0.0 is unit coverage of the deterministic core.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
All three product targets (app, widget, share extension) now build under
SWIFT_VERSION = 6.0 with zero errors and zero warnings. The UITests
target stays on 5.0: XCTestCase's nonisolated setUp/init overrides
conflict with the target's MainActor default isolation under 6, and test
tooling is not shipping code.
The original seven diagnostics, plus the layers Swift 6 mode surfaced
once those cleared:
- SMTPChannel is an actor. It was implicitly MainActor while running its
receive loop on a background queue, so parsedLines/lineWaiters/
receiveBuffer were declared main-actor-protected and mutated off it —
concurrent mutation while resuming a CheckedContinuation can
double-resume, which traps. The actor serialises all state; Network
callbacks hop in via Task. The start() continuation also gains an
OSAllocatedUnfairLock resume-once guard: the state handler can fire
.ready and later .failed, and resuming twice was a pre-existing trap of
the same family.
- CachedLookupResult is nonisolated (a value pair built inside actor
LookupRuntime cannot have a MainActor-bound memberwise init) with
conditional Sendable — opting out of MainActor isolation also opted out
of the implicit Sendable that globally-isolated types get.
- PortScanService.printableBanner is nonisolated: a pure transformation
called from the connection's queue.
- SweepActivityController stores the activity's Sendable id instead of
the non-Sendable Activity, re-resolving via Activity.activities inside
each fire-and-forget task, so nothing non-Sendable crosses isolation.
- App Intents' static title/description/openAppWhenRun become lets
(get-only protocol requirements; static var is shared mutable global
state), and the summary helpers are @MainActor to match the model
properties they read and the perform() implementations that call them.
- ExternalDataService's ISO8601DateFormatter is nonisolated(unsafe),
citing Apple's documented thread-safety, rather than risking a parser
behaviour change by switching APIs with no test coverage.
- TaskMetricsDelegate.metrics is nonisolated(unsafe): written on the
session's delegate queue, read only after the request completes, and
URLSession guarantees didFinishCollecting precedes task completion.
- The share extension extracts the host via async/withCheckedContinuation
instead of sending a non-Sendable completion into loadItem's @Sendable
handler; Task inherits the view controller's MainActor so the manual
DispatchQueue.main hop goes too.
Validated: clean Swift 6 build of all product targets, and the full
enforced 11-test audit suite green on the floor runtime — Swift 6's
runtime isolation checks ran the app through every screen without a
trap.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported on device: the Domain section header's Note button rendered
vertically — "N o t e", one character per line in a screen-tall capsule —
at a larger (not even accessibility-tier) text size. Same pathology the
row badges had: text inside a squeezed HStack compresses to a
one-character column instead of the layout adapting.
Three-part fix, mirroring the proven row treatment:
- CollapsibleSectionView's header is now a ViewThatFits: title, trailing
controls, and chevron on one line while they genuinely fit; otherwise
the trailing controls drop below the title row. Applies to every
section header, not just Domain.
- The Note and Track bordered buttons get .fixedSize() so their text can
never letter-wrap — their natural width is what pushes the header onto
its stacked layout.
- The "Tracked" pill becomes an icon-only indicator (eye in a tinted
circle) — with Pin and Note beside it the full pill was the first thing
to compress, and the word survives for VoiceOver via its label.
Enforced audit suite stays green (7 tests, 0 failures, floor runtime).
The post-lookup header state itself is not reachable by the harness — it
requires a live lookup — so on-device confirmation closes this out.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The point of the Phase 0 harness finally lands: named findings in
textClipped, dynamicType, hitRegion, elementDetection,
sufficientElementDescription, and trait now FAIL the build on the
empty-state suite. Regressions in five phases of accessibility work are
gated, not narrated.
Three carve-outs, each earned by evidence rather than convenience:
- contrast stays report-only. The two long-standing Settings findings are
rows scrolled under the translucent tab bar, and their attribution
flips between a row name and nil run-to-run — no suppression is narrow
enough to keep CI stable. The centralised palette is the real guard.
- The seeded dense-row tests run reportOnly. Bisection showed the audit
degrades on children-ignored content — the correct VoiceOver treatment
for dense rows — emitting unattributed contrast/dynamicType failures on
rows that measure 6-7:1 and render correctly.
- Characterised noise is suppressed narrowly and always logged as
[noise: reason]: disabled controls (WCAG 1.4.3 exempt), "nearly passed"
near-misses, system field placeholders (flagged at any length — proven
by shortening them to no effect), and unattributed clipped/dynamic-type
artifacts. noiseReason(for:) records each rule's provenance inline.
Validated in both directions. Positive: the full 11-test suite passes
with enforcement live, dark and light, on an erased simulator. Negative:
re-injecting the Phase 3 icon-exposure regression produced two named
[FAIL] findings and a failed suite — on both screens sharing the
component — then went green again on revert.
Docs and the CI workflow comment updated to describe the engaged state.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The dense rows and portfolio sections never rendered in the audit — the
test simulator has no tracked domains or batch results — so five phases
of row treatment shipped unmeasured. Driving the add-domain UI was tried
earlier and rejected (keyboard contamination, persistent state), so this
adds DOMAIN_DIG_SEED_FIXTURES: DEBUG-only launch argument, same pattern
as DOMAIN_DIG_FORCE_PRO_PLUS, seeding four tracked domains and four
batch results chosen to exercise every badge path, including a failed
lookup and a stress-length domain name.
Fixtures are strictly in-memory. persistTrackedDomains, refreshWidgetData
(App Group file), refreshPersistedData, and refreshMonitoringState are
all guarded while fixtures are active — the last one mattered: it runs
right after seeding in the app task and was reloading the empty disk
over the fixtures, which initially made the seeded watchlist audit pass
by silently auditing the empty state.
Four new audit tests cover the seeded Dashboard, Tracked Domains, and
batch results at default and AccessibilityXXXL.
What they found was real. At XXXL the watchlist row rendered the domain
as "hea lt…" while the Registered badge wrapped one character per line
into a screen-height capsule. Fixes, verified by before/after
screenshots and the XXXL audits dropping to 7-8 findings per screen:
- AppStatusBadgeView gets .fixedSize() — a capsule badge must never
letter-wrap; taking natural width instead forces the row layout to its
stacked alternative.
- WatchlistRowView, BatchResultRowView, and PortfolioExpiryRow headers
use ViewThatFits: domain-beside-badge while it genuinely fits, badge
below the domain at accessibility sizes. Domain titles get
fixedSize(horizontal: false, vertical: true) so they wrap rather than
report a single-line ideal width to ViewThatFits and truncate.
- The watchlist monitoring metadata strip (three texts abreast) stacks
vertically when it no longer fits instead of wrapping mid-word.
Known and deliberate: the seeded default-size audits still report a
contrast/dynamicType wave attributed to "unknown element". Bisecting the
row and badge accessibility modifiers showed most of it is an audit
artifact on children-ignored content (the same rows measure 6-7:1 and
render correctly); the artifact classes get characterised suppressions
when enforcement lands, not blanket ones.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A device checklist for everything the automated audit cannot reach:
VoiceOver speech, the More Content rotor, custom-content ordering,
completion announcements, Voice Control label-in-name, the colour/motion/
transparency settings simctl cannot toggle, iPad keyboard focus order,
and both system design languages (classic vs Liquid Glass).
Grounded in the actual implementation rather than generic guidance: each
item quotes the real label/announcement/field it verifies (e.g. "Refresh
all tracked domains", "Lookup complete for <domain>", the More Content
field order) and carries a [Px] tag pointing at the phase that introduced
it. Organised by iOS setting so each is toggled once, with a seed-data
step up front because the dense rows and widget are otherwise untestable
— the same reason they are unverified by the audit today. Includes a
sign-off matrix, a defect-log template, and the known-deferred items so a
ViewThatFits overflow is filed correctly rather than mistaken for a
regression.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Audit unchanged at 11 dark / 14 light — expected, as none of these
settings are exercised by performAccessibilityAudit, and simctl can
toggle only Increase Contrast, not Differentiate Without Color, Reduce
Motion, or Reduce Transparency. Correct by construction and build-clean;
runtime behaviour is verified in the Phase 6 manual pass.
Color independence:
- Widget status is now an SF Symbol (checkmark.circle.fill /
exclamationmark.triangle.fill / exclamationmark.octagon.fill), the same
vocabulary as the in-app badges, replacing a silent colour-only dot on
both the domain rows and the small-view count pills. Status now survives
greyscale and reads consistently across surfaces.
- Under accessibilityDifferentiateWithoutColor: the Dashboard
summary-card dot becomes a per-filter symbol, the selected quick-filter
chip gains a checkmark and a border (selection was fill-colour only, and
also gains the .isSelected trait), and LabeledValueRow prefixes a
warning/failure symbol. All gated on the setting so the default UI stays
uncluttered.
Reduce motion: all five withAnimation/.animation sites now pass nil under
accessibilityReduceMotion — AppCopyButton's check cross-fade,
CollapsibleSectionView's expand/collapse, TimelineDiffView's scroll, and
WatchlistView's list reorder.
Reduce transparency: the single .thinMaterial capsule falls back to an
opaque AppSurfaceElevated fill under accessibilityReduceTransparency.
The SweepActivityController item from the plan is dropped: it is pure
ActivityKit lifecycle with no animation, confirmed back in the issue
triage.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
phase 4)
The audit count is unchanged at 11 dark, and that is the expected
result: performAccessibilityAudit validates descriptions, traits,
contrast, hit regions, and clipping, but exercises none of VoiceOver's
speech, the More Content rotor, custom-content ordering, or
announcements — which is the entire substance of this phase. It is
verified by construction and stays green with no regressions; the
manual VoiceOver pass is Phase 6.
Icon-only controls (~14) get accessibilityLabel, obeying label-in-name:
where a control has visible text the label keeps it, so Voice Control
still works. The pin and bookmark toggles gain accessibilityValue and
.isSelected; the audit and workflow checkboxes gain .isSelected and a
hint. Decorative icons split out of Labels are hidden.
AppStatusBadgeView now reads as one word ("Critical"), not "icon,
Critical", via children: .ignore + label. SectionTitleView and
CollapsibleSectionView headers get the .isHeader trait for rotor
navigation; the collapsible header also exposes expanded/collapsed as a
value with a hint. The header deliberately does NOT use children:
.combine — its trailing() closure can hold Track/Pin controls, and
combining would swallow them.
Dense rows use combine-for-summary, custom-content-for-detail.
BatchResultRowView (8 elements) and WatchlistRowView (up to 9) become a
single element — domain as label, status as value — with risk, IP,
timestamp, source, certificate, and monitoring on the More Content
rotor, risk and certificate at .high importance. Reading all of it
inline would make a long sweep unnavigable. The custom-content chains
live in ViewModifiers because inlining six of them plus the layout broke
the type-checker. The shorter 3-4 element portfolio rows are left to
NavigationLink's automatic combine, per WWDC21-10121.
Technical strings get a speechStyle field on InfoRowViewData:
.technical applies speechAlwaysIncludesPunctuation and
accessibilityTextContentType(.sourceCode), set on DNS record values and
cipher suites so load-bearing punctuation is not swallowed.
Completion announcements: the sweep posts from the view model; the
single lookup posts from an onChange in the view, since resultsLoaded is
derived from many loading flags and has no single view-model moment.
Widget: each domain row was a silent 8pt status dot plus a bare "12d"
countdown. Rows now read as one phrase ("example.com, critical,
certificate expires in 12 days"); the count pills are labelled.
Not verifiable by the suite: the dense rows and the widget never render
in the audit (no tracked domains or batch results in the test
simulator), same limit as the deferred Phase 3 row reflow. Documented in
Docs/ACCESSIBILITY.md.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Takes the audit from 18 findings to 11 in dark mode. Everything that
remains is system-rendered or placeholder noise, characterised below.
The largest win was not where the plan expected. Every empty-state
heading reported as clipped text, and the cause was `Label`: it
constrains its own title, and `.fixedSize` applied to the Label does not
reach the `Text` inside. Splitting into `HStack { Image; Text }` and
putting the modifier on the Text cleared all four empty states at both
default and accessibility sizes.
That fix then caused a regression the audit caught immediately. `Label`
folds its image into the title's accessibility element; an HStack does
not, so the icon began announcing its raw SF Symbol name
("checklist.unchecked") to VoiceOver. Decorative icons split out of a
Label now carry .accessibilityHidden(true).
Tap targets:
- AppCopyButton was a literal 30x30 on nearly every data row. Now
@ScaledMetric from 44, floored at AppLayout.minimumTapTarget —
@ScaledMetric scales down below the default text size as well as up,
so the floor is load-bearing.
- controlMinHeight was 42 in compact density, putting every collapsible
section header and both Run buttons under the minimum.
Reflow:
- CardView's allowsHorizontalScroll defaulted to true, so nine call sites
hid content behind a horizontal gesture instead of wrapping — a WCAG
1.4.10 failure and the mechanism behind clipped rows at large text
sizes. The default is now false, and the remaining opt-in is suppressed
at accessibility sizes.
- Fixed .system(size:) point sizes replaced with text styles in the app
and the widget.
- The widget is clamped at accessibility1, the one place clamping is
correct: a widget canvas is a fixed size and WidgetKit truncates
overflow with no scroll affordance.
Two hypotheses were tested and discarded rather than left in. Monospaced
fonts looked like the clipping culprit — the app is 82% monospaced and
hyphenates mid-word at accessibility sizes — but switching the empty
state to proportional changed nothing, and prose typography is a design
decision rather than an accessibility fix. Shortening search prompts and
the domain placeholder also changed nothing: placeholder text is reported
clipped regardless of length, so "Search" is flagged exactly as "Search
portfolio" was.
Not done: ViewThatFits reflow for BatchResultRowView and
WatchlistRowView. Those rows never render in the audit because the test
simulator has no tracked domains or batch results, so any change there
would be unverifiable. Absence of findings is absence of data.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The selected Dashboard summary card rendered lavender, not blue. Its
background was a gradient of statusInfo at 28% and 12% opacity — a
dark-mode trick where a translucent accent over near-black reads as a
dim version of itself. Over a light background the same wash desaturates
toward violet.
Swapped for the authored StatusInfoSurface, which is a real colour with
a real contrast measurement rather than an emergent one. Same treatment
for the selected tag-filter chip (statusInfo at 30%) and an
intelligence-section badge (statusWarning at 14%).
The remaining opacity use, a 0.55 stroke in ContentView, is a border
rather than a text background and is left alone.
Verified across appearance and contrast settings on iOS 18.6: light 21
findings, light + Increase Contrast 18, dark 18, dark + Increase Contrast
18. Increase Contrast lowering the light count is the High Contrast
colorset variants working as intended on the system section headers.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported as "colors seem muted and hard to see on light mode", and
correct. The light palette optimised contrast and produced mud: #7A5600
reads olive rather than amber, #146C2E bottle-dark rather than green.
Contrast passed while the UI got harder to read, because hue identity is
what distinguishes warning from critical at a glance.
Two causes, both fixed.
Every foreground was required to clear 4.5:1 against its own 16% badge
tint — the harshest surface it ever sits on — which pushed each colour
about 20% darker than the common case needed. Most of what is actually on
screen is plain text on a card, with far more headroom. The fill is now
decoupled from the foreground: AppStatusTone carries a foreground and a
surface authored independently, with matching …Surface colorsets, so a
foreground no longer has to survive a wash of itself. Every status
foreground is now fully saturated.
And warning was yellow. Yellow cannot stay yellow at a lightness low
enough to pass 4.5:1 on white — it becomes olive. That is colorimetric,
not a tuning problem. Warning is now orange: #AD5100 light, #FF9F0A dark.
New light values: positive #008035, warning #AD5100, critical #CC0700.
Worst-case ratios 4.54–6.76 across page, card, and surface in both
schemes. Audit findings are unchanged — light 21, dark 18 — so the
vividness costs nothing.
Also picks up a literal .blue missed in phase 1: DomainDiffItem's
low-severity change colour, which the phase 1 sweep did not cover because
its pattern listed only cyan/yellow/green/red/orange/pink.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Removes the 16 scattered .preferredColorScheme(.dark) calls and the one
.toolbarColorScheme, and applies appearance in exactly one place — the
WindowGroup in DomainDigApp. Re-applying per view is what let the lock
spread across eight files unnoticed until light mode was unreachable.
Adds AppAppearance (System / Light / Dark) in @AppStorage, exposed under
Settings > Display next to Density. Honouring the system setting and
offering an override is one key, and it keeps the deliberate dark
aesthetic reachable for anyone who wants it.
Also replaces .secondary with AppTextSecondary across 191 sites. iOS's
own secondaryLabel is 3.29:1 on a light card — below AA — which never
showed while the app was locked to dark, where the same colour reads
6.32:1. Unlocking light mode is precisely what exposed it, so it belongs
here rather than in a later phase: without it, light mode would ship with
body text under 4.5:1 app-wide.
Dark mode reports 18 findings, unchanged from phase 1 — no regression
from unlocking. Light mode reports 21. The three extra are iOS-rendered
Section headers (TIER, PREFERENCES, SERVICES) using the system's grey;
overriding system header styling across every section to gain ~0.3:1 on
decorative labels is a poor trade and is left alone.
Two long-standing Settings contrast findings are now explained. They are
the last rows of a section sitting under the translucent tab bar, so the
audit measures text against a blended background — confirmed by
screenshot, present in dark mode since phase 0, and standard iOS
scroll-under behaviour rather than a defect.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
WCAG 1.4.3 exempts inactive components from contrast requirements, so
Inspect's Run button — disabled until a domain is typed — was reporting a
contrast failure that was never a real defect.
The first attempt typed a domain to enable the button. That worked for
the single-screen test but raised the keyboard, which then followed the
audit onto every later screen in the Dynamic Type sweep and reported nine
phantom hit-region findings per screen against the system emoji picker's
category buttons. Suppressing on the rule instead — drop contrast
findings whose element reports isEnabled == false — fixes it everywhere
with no UI manipulation.
Also documents that simulator keyboard state persists across runs, so a
dirty simulator inflates the burndown with system-UI findings. Erase and
re-run before believing anything that names system UI.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replaces every hard-coded colour with semantic asset colours that adapt to
light, dark, and Increase Contrast. Dark mode stays locked, so this is a
pure refactor: the audit reports the same findings before and after.
The accent is now blue rather than cyan, per the tech/DNS theme.
Why custom values rather than the system palette: every system colour
fails WCAG AA in light mode. Measured on white — systemYellow 1.51:1,
systemOrange 2.20:1, systemGreen 2.22:1, systemCyan 2.54:1, systemRed
3.55:1. All of them pass in dark mode, which is why the dark-locked app
looked fine, and why unlocking light mode was never a matter of deleting
.preferredColorScheme(.dark). Every new value clears 4.5:1 as text on its
page, its card, and its own 16% badge tint — the way AppStatusBadgeView
actually draws it.
The accent needed splitting in two. As text on a dark background it must
be light; as a fill behind a white label it must be dark. #4DA3FF reads
8.00:1 as text on black but 2.63:1 behind white text, so StatusInfo /
AccentColor cover the foreground role and AccentFill covers
.borderedProminent. AppOnAccent is the label colour for a solid fill and
flips by scheme.
Colours live in Shared/Colors.xcassets rather than the app catalog: the
Shared folder is already a synchronized group in all three targets, so
the widget and share extension pick the palette up with no project-file
surgery. AccentColor stays in the app catalog as the global tint — and is
now actually defined, having been an empty colorset that silently left
system controls rendering in stock blue while custom chrome used cyan.
Two deliberate visual changes: orange folds into StatusWarning and pink
into StatusCritical. They encoded the same severity as the colours they
now share, and both sites also carry a text label.
Audit findings drop 15 to 14, and one of the originals turned out to be
a phantom: the Inspect contrast failure was the Run button in its
disabled state, which WCAG 1.4.3 exempts. testInspectScreen now types a
domain first so the audit measures an enabled control. Findings also
carry the offending element now, so the remaining clipped-text items name
themselves ("No Portfolio Yet", "Search domains") instead of being
anonymous.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The two-job CI matrix was buying two near-identical iOS 26.x runs at
double the macOS minutes. GitHub's macos-26 image ships only 26.x
simulator runtimes, so it cannot test the 17.6 floor at all, and floor
coverage was the entire justification for the second job.
Split the work by what each side can uniquely do instead:
CI keeps one job on the newest runtime. Its real value is not the
runtime — it is building a clean checkout of the merge result, which
catches a file that was never committed. A local run cannot, and that
failure mode is live here: DomainDig.xcodeproj is hand-edited and uses
file-system-synchronized groups, where an entire missing folder still
builds locally. sr.ht cannot run macOS, so this is the only place that
check exists. Collapsing the matrix also removed the deployment-target
math, since "newest" is always above the floor.
Scripts/audit-a11y.sh runs the audit against real runtimes, defaulting
to floor + current. It reads the deployment target from the project
rather than hard-coding it, selects the oldest runtime at or above it
(one below is useless — the app cannot install), and says so plainly
when the nearest installed runtime is a major version above the target
rather than implying floor coverage it does not have.
.githooks/pre-push runs the floor tier, and only when Swift, asset, or
project files changed. Pre-push rather than pre-commit because the suite
takes ~85s: at pre-commit that blocks every commit, and a hook routinely
bypassed with --no-verify is worse than none. Opt in per clone with
'git config core.hooksPath .githooks'.
Docs/ACCESSIBILITY.md records the split, the measured non-nested
coverage that motivates it, and the enforcement ratchet.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two problems the first CI run exposed.
Audit timeouts. Three tests failed with "Audit failed to complete in
time" (code -56) on the GitHub runner. That is the audit's own internal
deadline on a slower machine, not an app defect, and the harness had no
resilience to it. Audits now retry up to three times, and a screen that
still cannot be audited is reported via XCTSkip rather than passing.
Skips are distinct from passes in CI, so an unaudited screen stays
visible instead of being silently counted as clean. The Dynamic Type
sweep attempts every screen before skipping, so one slow screen cannot
drop the other four.
Overclaimed floor coverage. The two-simulator matrix was justified on
covering the oldest supported OS, but the macos-26 image ships only iOS
26.x runtimes, so "floor" resolved to 26.2 and "current" to 26.5 — the
run compared two 26.x images and never touched an 18.x one. The measured
non-nested coverage that motivated the matrix (18.6 vs 27.0) reproduces
locally but not on this runner. The workflow comment now states this
plainly, and the selection step emits a warning annotation when the
resolved floor sits a major version or more above the deployment target,
so the gap is visible in the CI UI rather than assumed away.
Installing an older runtime in CI is possible via xcodebuild
-downloadPlatform but costs several GB and minutes per job; left out
pending a call on whether that trade is worth it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 0 of the accessibility pass: a regression guard that must exist
before any of the remedial phases, so their acceptance criteria are
enforced rather than asserted once by hand.
- Fix the project-level IPHONEOS_DEPLOYMENT_TARGET, which was 26.2 while
all three targets are 17.6. It was shadowed everywhere today, but any
target added later would silently inherit it and drop iOS 17.6 support
with no error.
- Add a DomainDigUITests target running performAccessibilityAudit on the
six primary screens, plus a sweep of every root screen at
AccessibilityXXXL. Uses the existing DOMAIN_DIG_FORCE_PRO_PLUS debug
argument so Pro-gated screens are reachable.
- Findings are reported, not failed. The audit surfaces violations that
exist today, so gating on them would block unrelated PRs until the
whole pass lands. Enforcement is a committed constant,
AccessibilityAuditHarness.enforcedAuditTypes, widened per audit type as
each phase clears a category.
- CI now runs xcodebuild test across two simulators. Audit coverage is
not nested between OS versions: on Tracked Domains, iOS 18.6 reported 2
findings and iOS 27.0 reported 6 (including contrast and
element-detection issues 18.6 never raised), while at accessibility
text sizes the Dashboard produced a hit-region finding on 18.6 that
27.0 did not.
- Simulator selection is now dynamic and floor-aware. The previous
selector took the first iPhone from any runtime, which can resolve to a
simulator below the deployment target where the app cannot install.
Baseline on iOS 18.6: 15 findings across 7 tests — text clipping on every
screen, contrast on Inspect and Settings, and a hit-region failure on the
Dashboard at accessibility text sizes.
|
| |
|
|
|
|
| |
Closes the last swift:S3358 in the new-code period. The hasChanges
branch nested a severity check inside the impactClassification ternary;
split into if/else. No behavior change.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Fixes the 4 reported bugs and ~97 code smells flagged in the new-code
period. No behavior changes.
Bugs (swift:S3923) — DomainInspectionService's confidenceFor* helpers each
returned `error == nil ? .low : .low`, an inert conditional. Simplified to
`return .low` and dropped the now-unused `error` parameter.
Smells:
- Merged 14 identical `.empty`/`.error` switch branches in DomainViewModel
- Consolidated duplicate implementations (clearPresentedResults/reset,
String.nonEmpty/nilIfEmpty, ExportFormat.id/fileExtension)
- Extracted nested ternaries into TLSGrade.tone, EmailSecurityGrade.tone,
and ChangeImpactClassification.color; removed ContentView.impactColor
and the duplicate mapping in BatchResultsView
- Documented empty closures and singleton inits
- Marked unused protocol-conformance parameters `_`
- Renamed CloudSyncTrigger.`import` to `imported` (raw value preserved)
and SSLSessionDelegate's _serverTrust/_tlsMetadata
- Merged nested ifs in the DER parser; flattened closure nesting in
PortScanService and IntegrationService
- Replaced two-case switches with if/else
Left open: S107 (init parameter counts), S115 (constants mirroring DoH and
ipapi JSON keys), S1075 (false positives on https:// literals), and two
S117 hits on SwiftUI $binding shorthand. These want a Won't Fix resolution
in SonarCloud, not a code change.
|
| |
|
|
|
|
|
| |
The badge added alongside the SonarCloud badges claimed GPL v3, and the
README's License section claimed 'GPL 3.0 or later'. LICENSE has always
been MIT, so both statements misrepresented the project's terms on its
public front page. The section text predates this branch.
|
| | |
|
| |
|
|
|
|
|
|
| |
Bump AppVersion/marketing version to 4.8.2 and build number to 42 across the
app, widget, and share extension targets. Mark v4.8.2 (delivery visibility for
disabled integrations, forced queue retries, unreachable-domain reporting,
Swift 6 concurrency warnings, synced StoreKit configuration) shipped in
RELEASE_ROADMAP.md and record the UAT follow-ups as resolved.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closes #10.
resolvedSnapshotAfterFallback replaces a failed lookup's snapshot with
the previous one, so alertDescriptor compared the old snapshot against
itself, found matching hashes, and the run reported 'No meaningful
changes' for a domain that was never actually reached. Nothing in the
UI or the monitoring log distinguished that from a genuine no-change.
MonitoringDomainResult now carries unreachableReason, set when the
fallback fires. It is Optional so already-persisted monitoring logs
still decode. The run summary reads 'Could not check — kept the previous
result' with the underlying error, and monitoringEvents emits a warning-
severity monitoringFailure so configured integrations hear about it
rather than seeing silence.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closes #8, closes #9.
enqueue(events:) filtered to enabled targets before writing any
DeliveryRecord, so events routed to a disabled integration disappeared
with nothing in the Delivery Log. Disabled targets now record a .skipped
entry with a reason, matching how filter mismatches are already surfaced.
sendTest bypassed the isEnabled check entirely, so a test event
delivered against a target that silently dropped every real event —
exactly the wrong signal when someone is verifying their setup. It now
skips with the same reason.
processQueueNow only restarted the processing task; it never moved
nextAttemptAt, so an item in retry backoff stayed undue and the fresh
task went straight back to sleep. Backoff reaches an hour, so the button
appeared inert for the one case it exists to handle. It now pulls every
queued item forward, and reports when the queue is empty instead of
returning silently.
|
| |
|
|
|
|
|
|
|
|
|
| |
DomainDig.storekit sat beside the DomainDig/ source folder, and Xcode
treated the two as conflicting when creating the synced configuration —
the resulting confirmation dialog offered to replace the folder. Naming
the file SyncedProducts.storekit removes the collision entirely and
matches the convention Xcode uses for synced configurations elsewhere.
Updates the project file reference and the scheme's
StoreKitConfigurationFileReference to match.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Xcode synced DomainDig.storekit against App Store Connect, replacing the
hand-authored placeholders with real values: the app's internal ID
(6760368004), the real subscription group ID (22051301), a synchronized
timestamp, and group localizations.
Also registers the file in the project so Xcode can find and re-sync it,
but without target membership. Xcode's default added it to the Resources
build phase of all three targets, which would ship the test configuration
inside the app, widget, and share extension bundles. The scheme
references it by path for the Run action; it does not need bundling.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Compared against a known-good Xcode-generated configuration
(hutch/SyncedProducts.storekit) and corrected the hand-authored file:
- version bumped 4.0 -> 5.0, the format Xcode 26 writes
- added the appPolicies block Xcode always emits
- added the missing settings keys (_askToBuyEnabled,
_billingGracePeriodEnabled, _billingIssuesEnabled, _disableDialogs,
_renewalBillingIssuesEnabled, _timeRate)
- _developerTeamID set to ZCNAX3VL9D, matching DEVELOPMENT_TEAM in the
project rather than being left empty
- _applicationInternalID cleared. It previously held 6763610682, which
is the Apple ID of the Pro+ (Annually) subscription, not the app's
internal ID. That value is now on the subscription's internalID where
it belongs; the app ID is left empty rather than wrong.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The StoreKitConfigurationFileReference added in #11 used one '../' too
many, resolving outside the repository. Xcode resolves it relative to
the .xcodeproj's xcshareddata directory, so two levels reaches the repo
root.
SweepActivityAttributes is now explicitly nonisolated. The app target
sets SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor while the widget target
does not, so a type shared by both inferred a main-actor-isolated
ActivityAttributes conformance that ActivityKit cannot use from its
concurrent contexts.
LocalAPIService's logger closures captured self strongly while their
inner Tasks declared [weak self]. The weak capture is now on the outer
closure and bound before the Task, so the concurrently-executing closure
references an immutable strong local rather than the weak capture.
|
| |
|
|
|
|
|
| |
PR #11 merged after the v4.8.1 version bump, so the roadmap's v4.8.1
section omitted the product ID correction and the StoreKit configuration
file. Both are in the 4.8.1 tree; document them alongside the other
fixes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds DomainDig.storekit mirroring the App Store Connect setup: one
subscription group with Pro+ at level 1 and Pro at level 2, using the
corrected product IDs.
Wires it into the Run action so purchases resolve locally against
StoreKit instead of the App Store, and disables the
DOMAIN_DIG_FORCE_PRO_PLUS launch argument, which bypasses StoreKit
entirely and would mask whether the purchase path works.
Prices in the configuration are local-testing placeholders and do not
need to match App Store Connect.
|