summaryrefslogtreecommitdiff
path: root/Docs
Commit message (Collapse)AuthorAgeFilesLines
* convert readme to nfo; convert docs to txt; relicense to 0bsdHEADmainChristian Cleberg37 hours4-3/+3
|
* update org nameChristian Cleberg43 hours1-1/+1
|
* docs: polish repo documentation after the v5.0.0 workChristian Cleberg10 days1-5/+14
| | | | | | | | | | | | | | | | | | | Brings the docs in line with the shipped v5.0.0 state and fixes staleness that accumulated across the release. - README: add a Tests section documenting the DomainDigTests unit net and that the scheme's test action runs both it and the accessibility audit; reword the roadmap pointer (v5.0.0 shipped, no longer "planned"); align the contact address to [email protected] (was cleberg.net, inconsistent with SECURITY.md). - ARCHITECTURE: drop the stale "v4.4.1" from the title; note DomainReportExporter also renders Markdown and PDF; describe the DomainViewModel concern extensions and the ContentView split (SettingsViews / ResultSectionViews); record that v5.0.0 stabilized the Local API contract with links to local-api.md and data-migration.md; add a Testing section covering both test targets. - SECURITY: bump the supported-versions table from 4.x to 5.x. ACCESSIBILITY.md, local-api.md, and data-migration.md were already current and are unchanged. Docs-only; no code changes.
* feat: versioned store-migration policy for persisted data (v5 step 2)Christian Cleberg10 days1-0/+94
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat: stabilize and document the Local API v1 response contract (v5 step 3)Christian Cleberg10 days1-0/+113
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* docs cleanupChristian Cleberg11 days17-638/+0
|
* test(a11y): Phase 6 verification — metadata assertions, middle-band sweep, ↵Christian Cleberg11 days16-0/+292
| | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(a11y): engage the audit enforcement ratchet (#21)Christian Cleberg13 days1-21/+32
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* docs(a11y): add Phase 6 manual verification checklist (#21)Christian Cleberg13 days1-0/+346
| | | | | | | | | | | | | | | | | | | 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.
* feat(a11y): color independence, reduce motion, reduce transparency (#21 phase 5)Christian Cleberg13 days1-0/+25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(a11y): VoiceOver labels, dense-row rotor content, announcements (#21 ↵Christian Cleberg13 days1-0/+43
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(a11y): Dynamic Type reflow and tap targets (#21 phase 3)Christian Cleberg2026-07-201-0/+21
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(a11y): rebalance the light palette so hues surviveChristian Cleberg2026-07-201-5/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(a11y): unlock light mode and add appearance preference (#21 phase 2)Christian Cleberg2026-07-201-0/+26
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(a11y): suppress disabled-control contrast findings on the ruleChristian Cleberg2026-07-201-6/+13
| | | | | | | | | | | | | | | | | | 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.
* feat(a11y): semantic colour system (#21 phase 1)Christian Cleberg2026-07-201-0/+58
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* ci: split accessibility coverage between local runtimes and CIChristian Cleberg2026-07-201-0/+96
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* DomainDig v4.4.1: Consolidate Audit Mode and remove the CLI targetv4.4.1Christian Cleberg2026-07-161-28/+73
| | | | | | | | | | | - Make DomainDig/DomainDig/Audit* the single active Audit Mode implementation (models, views, exporter) with an Audit tab and session/export UI - Include audit sessions in backup/restore lifecycle counts, summaries, and merge behavior via DomainDataPortabilityService - Remove the DomainDigCLI target, source file, scheme, and all project references; keep the shared inspection/report pipeline for the app - Align AppVersion.current to 4.4.1 and refresh README/architecture docs - Add RELEASE_ROADMAP.md
* feat(v3.0.0): unify platform architecture and introduce feature tiersChristian Cleberg2026-04-221-0/+48
* consolidate DomainReport as canonical data model * add feature tier system (free/pro/data+ scaffolding) * apply clean feature gating across workflows and tracking * refactor app structure for maintainability * standardize navigation and settings * add data lifecycle controls * ensure consistency across UI, export, and CLI * stabilize internal inspection API