Repository navigation
[Core][iOS] HTML export: an opponent's-perspective page, led by an App Store strip - #138
Conversation
GameScoreLabel and PointMatchScore only ever formatted scores in the
recorder's frame, so anything shown to the opponent read backwards
("Ad Me" for the recorder's advantage). Both now take an optional
`focal:` (default `.me`, so every existing caller is unchanged). Raw
`games` stay recorder-oriented; only the labels flip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re strip The export was recorder-framed, so a page sent to the opponent told them they lost a match they won - in the static preview and again when the interactive page rebuilt. MatchWebViewModel.make now takes `perspective:`; every "me" field (result, scores, points, chips, score labels, comparison, points-won) is the reader's side, so the static fallback and the viewer need no perspective logic and cannot disagree. `.me` output is unchanged apart from the new fields. The recorder's HR/steps stay in the opponent's page, labelled "Opp", because they help the opponent's AI prompt; HR-zone win rates and PulseCoach pair the recorder's HR with the reader's results, so only the recorder's page keeps them (the MatchExporter rule). The opponent's page offers only the opponent AI prompt. Every page now opens with a "Tracked with DeuceMate" strip linking to the App Store and the marketing site: the recipient usually doesn't have the app. The URLs live once, in MatchWebViewModel+Promo. They are tap-only links; the page still loads nothing on open. The tests' "no https://" checks become an exact allowlist (the two promo URLs plus the AI hosts). Schema v12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The share menu's Interactive Web Page is now a submenu: My Perspective or Opponent's Perspective. Both files are built off the main thread in the existing task (the opponent's as ..._opponent.html) and each share goes through the existing health disclosure for its perspective. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Updates CLAUDE.md, the export design doc, the user guide, the file inventory (new MatchWebViewModel+Promo.swift), and the lifecycle and health-data-flow notes that described a single recorder-framed page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01b1ad988d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| |---|---:|---|---| | ||
| | `Views/PointsGraphView.swift` | 1,870 | The interactive match chart: points momentum over time with counted Me/Opp filters for outcomes, serving categories and ending shots, plus optional heart-rate and steps overlays; pinch-zoom, fullscreen, touch-to-inspect. Serving hides alongside Outcomes when no categorised points exist; shared matching excludes uncategorised serve counts and marks in mixed matches. Its selection summary uses reducer-derived game/match scores after the selected rally, matching cumulative point totals; serving status describes that rally and set-relative numbering matches the Points tab. | Stats & graphs | | ||
| | `Views/MatchDetailView.swift` | 1,296 | One match's detail page: score summary, End at Current Score for in-progress records, stats tabs, the graph, coaching sections, storage-location and iCloud badges, and the export / AI-coach actions — including "Share Interactive Web Page", a self-contained `.html` file (built off-thread, shared via the system share sheet) the opponent can open in any browser. Text/HTML share actions **and the AI Coach hand-off** route through one per-export HealthKit disclosure (`HealthExportConsent`, Blocker 4): a shared "Share health data?" alert (via `PendingHealthDisclosure`) when the match carries health data — Share → `UIActivityViewController` (`ShareSheet`), Continue → the AI Coach sheet; health-free matches proceed directly. Its Points tab rows show the full pre-point match score and serving side with a combined accessibility label. | Stats, AI export, web export, archive, health/privacy | | ||
| | `Views/MatchDetailView.swift` | 1,296 | One match's detail page: score summary, End at Current Score for in-progress records, stats tabs, the graph, coaching sections, storage-location and iCloud badges, and the export / AI-coach actions — including the "Interactive Web Page" submenu (My Perspective / Opponent's Perspective), a self-contained `.html` file per reader (both built off-thread, shared via the system share sheet) the opponent can open in any browser — the opponent's copy framed from their side. Text/HTML share actions **and the AI Coach hand-off** route through one per-export HealthKit disclosure (`HealthExportConsent`, Blocker 4): a shared "Share health data?" alert (via `PendingHealthDisclosure`) when the match carries health data — Share → `UIActivityViewController` (`ShareSheet`), Continue → the AI Coach sheet; health-free matches proceed directly. Its Points tab rows show the full pre-point match score and serving side with a combined accessibility label. | Stats, AI export, web export, archive, health/privacy | |
There was a problem hiding this comment.
Refresh the MatchDetailView inventory count
This modified inventory row still reports 1,296 lines, while MatchDetailView.swift is now 1,323 lines after this change. The repository guide explicitly requires outdated file sizes in architecture documentation to be corrected in the same change, so update this count and audit the counts for the other production files expanded here.
AGENTS.md reference: AGENTS.md:L14-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in fe43467: updated MatchDetailView.swift to 1,323 and audited every production file this PR touches. Also refreshed PointMatchScore (188), SetScoreLabel (80), MatchWebStaticFallback (~450), MatchWebTemplate (~1,055), MatchWebViewModel+Build (~550) and MatchWebViewModel (~480); the rest were still within their stated approximations. CLAUDE.md's ~1300 for MatchDetailView remains accurate.
MatchDetailView, PointMatchScore, SetScoreLabel and four WebExport files grew in this PR; their inventory sizes were stale (review feedback). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
The interactive HTML export was framed from the recorder's side only. Sent to an opponent who won, it told them they lost: in the static preview (Messages / Quick Look) and again when the interactive page rebuilt in a browser. The recipient also usually doesn't have DeuceMate, and the page gave them no way to find it.
What changed
Two perspectives. The share menu's Interactive Web Page is now a submenu: My Perspective / Opponent's Perspective.
MatchWebViewModel.make(from:maxHR:perspective:)builds the page for its reader. Every "me" field (result badge, score and set lines, points, cumulative lines, chips, game and match score labels, the per-set comparison, points-won) is the reader's side. The opponent's page says Won when the recorder lost.GameScoreLabelandPointMatchScoregain an optionalfocal:(default.me; existing callers unchanged)..meoutput is unchanged apart from the new fields.Health data in the opponent's page. The recorder's per-point HR/steps and totals stay (useful to the opponent's AI prompt) and are labelled "Opp" (
Opp Heart Rate,Opp HR:,Opp Steps). HR-zone win rates and PulseCoach pair the recorder's HR with the reader's results, so only the recorder's page keeps them. That's the ruleMatchExporteralready applies to the opponent's text export. The opponent share uses the existing disclosure (.opponent+ raw points), which lists exactly these fields.App Store strip. Every page (both perspectives, both layers) now opens with a "Tracked with DeuceMate" strip linking to the App Store and the marketing site. The URLs live once, in the new
MatchWebViewModel+Promo.swift. They are tap-only links; the page still loads nothing on open.Schema v12 (v11 = perspective, v12 = promo; v11 was never released).
Tests changed on purpose (please review)
test_comparison_sectionsAndGating_fullMatchpinnedschemaVersion == 10; it now expects 12.https://. Adding the store links breaks that by design, so those checks now callassertOnlyOptInLinks. Everyhttps://must be exactly the App Store URL or the website URL (or, in the AI test, a known AI host). Any other link, including another path on the same hosts, fails. Thesrc=/<link/@import/ CDN /http://checks are unchanged.No test was deleted, skipped, or given a looser expected value otherwise.
New tests cover:
Verification (local, macOS)
swift testinDeuceMateCore: 630 tests, 0 failures.xcodebuild build(iOS Simulator): succeeded.DeuceMateTestsunit tests: TEST SUCCEEDED (run before the promo commit; the iOS target only changed inMatchDetailView).Not verified: the share sheet and submenu on a physical iPhone, and opening the files from Messages on a device. The watch app is untouched.
Docs
CLAUDE.md,INTERACTIVE_HTML_EXPORT_PLAN.md,USER_GUIDE.md,file-inventory.md(newMatchWebViewModel+Promo.swift),match-lifecycle.md,health-data-flow.md, and TECHNICAL_DEBT #25's wording. The published support page (docs/website/) is unchanged; its description is still accurate.🤖 Generated with Claude Code