<feed xmlns='http://www.w3.org/2005/Atom'>
<title>domain-dig.git/DomainDig, branch v5.0.0</title>
<subtitle>local-first ios domain inspection toolkit. dns, tls, rdap, audit.
</subtitle>
<id>http://git.krz.sh/krz/domain-dig.git/atom?h=v5.0.0</id>
<link rel='self' href='http://git.krz.sh/krz/domain-dig.git/atom?h=v5.0.0'/>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/'/>
<updated>2026-07-25T16:18:41+00:00</updated>
<entry>
<title>chore: cut v5.0.0 release (version bump)</title>
<updated>2026-07-25T16:18:41+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:31:51+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=cc1cb343b19b662ef59f9700702ed285e4591029'/>
<id>urn:sha1:cc1cb343b19b662ef59f9700702ed285e4591029</id>
<content type='text'>
Bumps the app to 5.0.0 now that all four v5.0.0 workstreams have landed:

- AppVersion.current 4.9.0 -&gt; 5.0.0
- MARKETING_VERSION 4.9.0 -&gt; 5.0.0 across all targets (app, widget, share
  extension, and both test bundles)
- CURRENT_PROJECT_VERSION 44 -&gt; 45 across all targets
- RELEASE_ROADMAP.md: v5.0.0 marked shipped, "Current version" -&gt; 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.
</content>
</entry>
<entry>
<title>refactor: extract result section views into ResultSectionViews (v5 step 4, 7/n)</title>
<updated>2026-07-25T05:23:45+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:22:34+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=0571e751f8d6f2975b7ea9fdbd30f5b77c05e836'/>
<id>urn:sha1:0571e751f8d6f2975b7ea9fdbd30f5b77c05e836</id>
<content type='text'>
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 -&gt; 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 -&gt; 1626 lines (3881 at the start of the ContentView
pivot; -2255 total). App builds clean; unit suite 58/58. No project.pbxproj
change.
</content>
</entry>
<entry>
<title>refactor: extract Settings screens into SettingsViews.swift (v5 step 4, 6/n)</title>
<updated>2026-07-25T05:16:00+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:15:15+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=48999737a37ef16cad0d84e71fa1b229bb04903f'/>
<id>urn:sha1:48999737a37ef16cad0d84e71fa1b229bb04903f</id>
<content type='text'>
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 &amp; Network, iCloud Sync, Local API, Monitoring, Import &amp;
  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 -&gt; 2739 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
</content>
</entry>
<entry>
<title>refactor: extract history query surface into DomainViewModel+History (v5 step 4, 5/n)</title>
<updated>2026-07-25T05:11:47+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:10:46+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=321a10f85435fdcbfb54f109ee53df895a550a42'/>
<id>urn:sha1:321a10f85435fdcbfb54f109ee53df895a550a42</id>
<content type='text'>
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 -&gt; 4171 lines (4864 at the start of step 4; -693
total). App builds clean; unit suite 58/58. No project.pbxproj change.
</content>
</entry>
<entry>
<title>refactor: extract workflow surface into DomainViewModel+Workflows (v5 step 4, 4/n)</title>
<updated>2026-07-25T05:07:52+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:05:53+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=9092ab60f82152cc4ae38677c3af4a76013f4e54'/>
<id>urn:sha1:9092ab60f82152cc4ae38677c3af4a76013f4e54</id>
<content type='text'>
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 -&gt; 4306 lines (4864 at the start of step 4).
App builds clean; unit suite 58/58. No project.pbxproj change.
</content>
</entry>
<entry>
<title>refactor: extract export/portability into DomainViewModel+Export (v5 step 4, 3/n)</title>
<updated>2026-07-25T05:00:29+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T04:59:07+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=e5d6c6c30df78e1d9b07088c256421aa663cb173'/>
<id>urn:sha1:e5d6c6c30df78e1d9b07088c256421aa663cb173</id>
<content type='text'>
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 -&gt;
  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 -&gt; 4434 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
</content>
</entry>
<entry>
<title>refactor: extract monitoring surface into DomainViewModel+Monitoring (v5 step 4, 2/n)</title>
<updated>2026-07-25T05:00:29+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T04:52:39+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=7805888980f6d41bcc881c5d2e6001b2d8e69351'/>
<id>urn:sha1:7805888980f6d41bcc881c5d2e6001b2d8e69351</id>
<content type='text'>
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 -&gt; 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 -&gt; 4601 lines. App builds clean; unit suite 58/58.
No project.pbxproj change (DomainDig/ is a synchronized group).
</content>
</entry>
<entry>
<title>refactor: extract audit surface into DomainViewModel+Audit (v5 step 4, 1/n)</title>
<updated>2026-07-25T05:00:29+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T04:45:56+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=e597cdf5b7084634e300af39b622c8552c9cc5e8'/>
<id>urn:sha1:e597cdf5b7084634e300af39b622c8552c9cc5e8</id>
<content type='text'>
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+&lt;Concern&gt;.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 -&gt; 4761 lines. App builds clean; unit suite 58/58.
</content>
</entry>
<entry>
<title>feat: add Copy cURL Command button to the Local API page (#30)</title>
<updated>2026-07-25T00:09:01+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-24T23:46:47+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=22ad96331a4456662f740c48882cab8aadd6d0b4'/>
<id>urn:sha1:22ad96331a4456662f740c48882cab8aadd6d0b4</id>
<content type='text'>
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 &lt;token&gt;" http://127.0.0.1:&lt;port&gt;/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().
</content>
</entry>
<entry>
<title>fix: sync CloudKit through a custom zone instead of queries (#29)</title>
<updated>2026-07-24T23:32:24+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-24T23:17:09+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=2ed92d773a2505ac3b6fbd3d280258549936ba2a'/>
<id>urn:sha1:2ed92d773a2505ac3b6fbd3d280258549936ba2a</id>
<content type='text'>
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.
</content>
</entry>
</feed>
