From 1563a4c06782a285f527616fde57dea46d0ad4d5 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Sun, 24 May 2026 22:36:06 -0400 Subject: [PATCH] add webview inspection diagnostics --- .beads/issues.jsonl | 2 + Dreamio/DreamioWebViewController.swift | 225 ++++++++++++++ ...le-web-inspector-playback-diagnostics.html | 282 ++++++++++++++++++ 3 files changed, 509 insertions(+) create mode 100644 docs/turns/2026-05-24-enable-web-inspector-playback-diagnostics.html diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1cab55d..7bfcea4 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,2 +1,4 @@ +{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-evt","title":"Enable WebView inspection and playback diagnostics","description":"Add development-only WKWebView inspection and token-safe playback diagnostics so Dreamio can debug hosted Stremio media failures without changing app navigation, login, or playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T02:30:26Z","created_by":"dirtydishes","updated_at":"2026-05-25T02:34:55Z","started_at":"2026-05-25T02:30:32Z","closed_at":"2026-05-25T02:34:55Z","close_reason":"Implemented debug-only WKWebView inspection, token-safe playback diagnostics, navigation logging, validation build, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index 6d0376a..eac3597 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -4,6 +4,7 @@ import WebKit final class DreamioWebViewController: UIViewController { private enum Constants { static let stremioWebURL = URL(string: "https://web.stremio.com/")! + static let diagnosticsMessageHandler = "dreamioDiagnostics" } private lazy var webView: WKWebView = { @@ -12,6 +13,13 @@ final class DreamioWebViewController: UIViewController { configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] configuration.preferences.javaScriptCanOpenWindowsAutomatically = true +#if DEBUG + configuration.userContentController.add( + WeakScriptMessageHandler(delegate: self), + name: Constants.diagnosticsMessageHandler + ) + configuration.userContentController.addUserScript(Self.playbackDiagnosticsScript) +#endif let webView = WKWebView(frame: .zero, configuration: configuration) webView.translatesAutoresizingMaskIntoConstraints = false @@ -20,6 +28,11 @@ final class DreamioWebViewController: UIViewController { webView.navigationDelegate = self webView.uiDelegate = self webView.scrollView.contentInsetAdjustmentBehavior = .never +#if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } +#endif return webView }() @@ -32,6 +45,96 @@ final class DreamioWebViewController: UIViewController { private var progressObservation: NSKeyValueObservation? +#if DEBUG + private static let playbackDiagnosticsScript = WKUserScript( + source: """ + (() => { + if (window.__dreamioPlaybackDiagnosticsInstalled) { + return; + } + window.__dreamioPlaybackDiagnosticsInstalled = true; + + const post = (type, payload = {}) => { + try { + window.webkit.messageHandlers.dreamioDiagnostics.postMessage({ + type, + payload, + href: window.location.href + }); + } catch (_) {} + }; + + const describeValue = (value) => { + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack + }; + } + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value); + } catch (_) { + return String(value); + } + }; + + ["error", "warn"].forEach((level) => { + const original = console[level]; + console[level] = (...args) => { + post(`console.${level}`, { args: args.map(describeValue) }); + original.apply(console, args); + }; + }); + + window.addEventListener("unhandledrejection", (event) => { + post("unhandledrejection", { + reason: describeValue(event.reason) + }); + }); + + const videoState = (video) => ({ + currentSrc: video.currentSrc || video.src || "", + networkState: video.networkState, + readyState: video.readyState, + errorCode: video.error ? video.error.code : null, + errorMessage: video.error ? video.error.message : null + }); + + const attachVideoDiagnostics = (video) => { + if (!video || video.__dreamioDiagnosticsAttached) { + return; + } + video.__dreamioDiagnosticsAttached = true; + video.addEventListener("error", () => { + post("video.error", videoState(video)); + }, true); + }; + + document.querySelectorAll("video").forEach(attachVideoDiagnostics); + + new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node instanceof HTMLVideoElement) { + attachVideoDiagnostics(node); + } + if (node.querySelectorAll) { + node.querySelectorAll("video").forEach(attachVideoDiagnostics); + } + }); + }); + }).observe(document.documentElement, { childList: true, subtree: true }); + })(); + """, + injectionTime: .atDocumentStart, + forMainFrameOnly: false + ) +#endif + override func viewDidLoad() { super.viewDidLoad() @@ -77,6 +180,70 @@ final class DreamioWebViewController: UIViewController { }) present(alert, animated: true) } + +#if DEBUG + private func logDiagnostic(type: String, payload: Any, pageURL: String?) { + let redactedPageURL = pageURL.map(redactedURLString) ?? "unknown" + let redactedPayload = redactDiagnosticValue(payload) + print("[DreamioDiagnostics] \(type) page=\(redactedPageURL) payload=\(redactedPayload)") + } + + private func redactDiagnosticValue(_ value: Any) -> Any { + switch value { + case let string as String: + return redactedURLString(string) + case let array as [Any]: + return array.map(redactDiagnosticValue) + case let dictionary as [String: Any]: + return dictionary.reduce(into: [String: Any]()) { result, entry in + result[entry.key] = redactDiagnosticValue(entry.value) + } + default: + return value + } + } + + private func redactedURLString(_ value: String) -> String { + guard var components = URLComponents(string: value), components.scheme != nil else { + return redactTokenLikeFragments(in: value) + } + + components.query = nil + components.fragment = nil + if let path = components.percentEncodedPath.nilIfEmpty { + components.percentEncodedPath = redactTokenLikePathSegments(in: path) + } + return redactTokenLikeFragments(in: components.string ?? value) + } + + private func redactTokenLikePathSegments(in path: String) -> String { + path + .split(separator: "/", omittingEmptySubsequences: false) + .map { segment -> String in + let text = String(segment) + if text.range(of: #"^[A-Za-z0-9_-]{24,}$"#, options: .regularExpression) != nil { + return "[redacted]" + } + return text + } + .joined(separator: "/") + } + + private func redactTokenLikeFragments(in value: String) -> String { + let patterns = [ + #"(?i)((?:token|access_token|auth|signature|sig|key|apikey|api_key|jwt|session|password)=)([^&\s]+)"#, + #"(?i)(bearer\s+)[A-Za-z0-9._~+/=-]+"# + ] + + return patterns.reduce(value) { redacted, pattern in + redacted.replacingOccurrences( + of: pattern, + with: "$1[redacted]", + options: .regularExpression + ) + } + } +#endif } extension DreamioWebViewController: WKNavigationDelegate { @@ -99,11 +266,31 @@ extension DreamioWebViewController: WKNavigationDelegate { decisionHandler(.allow) } + func webView( + _ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void + ) { +#if DEBUG + if let response = navigationResponse.response as? HTTPURLResponse { + let url = response.url?.absoluteString ?? "unknown" + print("[DreamioNavigation] status=\(response.statusCode) url=\(redactedURLString(url))") + } +#endif + decisionHandler(.allow) + } + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { +#if DEBUG + print("[DreamioNavigation] didFail url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))") +#endif showLoadFailure(error) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { +#if DEBUG + print("[DreamioNavigation] didFailProvisional url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))") +#endif showLoadFailure(error) } @@ -124,6 +311,44 @@ extension DreamioWebViewController: WKNavigationDelegate { } } +#if DEBUG +extension DreamioWebViewController: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.name == Constants.diagnosticsMessageHandler, + let body = message.body as? [String: Any], + let type = body["type"] as? String + else { + return + } + + logDiagnostic( + type: type, + payload: body["payload"] ?? [:], + pageURL: body["href"] as? String + ) + } +} + +private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + weak var delegate: WKScriptMessageHandler? + + init(delegate: WKScriptMessageHandler) { + self.delegate = delegate + super.init() + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + delegate?.userContentController(userContentController, didReceive: message) + } +} + +private extension String { + var nilIfEmpty: String? { + isEmpty ? nil : self + } +} +#endif + extension DreamioWebViewController: WKUIDelegate { func webView( _ webView: WKWebView, diff --git a/docs/turns/2026-05-24-enable-web-inspector-playback-diagnostics.html b/docs/turns/2026-05-24-enable-web-inspector-playback-diagnostics.html new file mode 100644 index 0000000..116c281 --- /dev/null +++ b/docs/turns/2026-05-24-enable-web-inspector-playback-diagnostics.html @@ -0,0 +1,282 @@ + + + + + + Dreamio Web Inspector and Playback Diagnostics + + + + +
+
+
Repository implementation turn
+

Dreamio Web Inspector and Playback Diagnostics

+

Enabled development-only Safari inspection for Dreamio's WKWebView and added token-safe diagnostics for console warnings, promise rejections, video failures, navigation errors, and HTTP navigation statuses.

+
+ +
+

Summary

+

This pass improves observability without changing Dreamio's login, navigation, addon browsing, or playback behavior. Debug builds on iOS 16.4 and newer now opt the WebView into Safari Web Inspector, and page diagnostics flow back to Xcode logs with URL queries, fragments, bearer tokens, and long token-like path segments redacted.

+
+ +
+

Changes Made

+
    +
  • Enabled webView.isInspectable for DEBUG builds on iOS 16.4 and newer.
  • +
  • Installed a small WKUserScript at document start to observe console warnings, console errors, unhandled promise rejections, and dynamically inserted <video> elements.
  • +
  • Added native logging for WKNavigationDelegate response statuses and load failures.
  • +
  • Added native redaction helpers before diagnostic data is printed.
  • +
  • Kept all diagnostics behind #if DEBUG so release builds do not expose the inspection or message bridge surface.
  • +
+
+ +
+

Context

+

The current Debridio VOD failure appears to need browser-level evidence: stream URL shape, request headers, response metadata, MIME type, and JavaScript media errors. Before adding a native player path, Dreamio needs a reliable way to inspect hosted Stremio Web inside the app and collect media failure details from the page itself.

+
+ +
+

Important Implementation Details

+
    +
  • The diagnostics bridge posts messages through window.webkit.messageHandlers.dreamioDiagnostics.
  • +
  • The user script attaches to existing videos and videos inserted later through a MutationObserver.
  • +
  • Video diagnostics include networkState, readyState, currentSrc, media error code, and media error message.
  • +
  • The native logger strips URL query strings and fragments, redacts obvious token-like key values, redacts bearer credentials, and replaces long token-like path segments.
  • +
  • A weak message-handler wrapper avoids the common WKUserContentController retain cycle.
  • +
+
+ +
+

Relevant Diff Snippets

+

The snippets below are rendered with @pierre/diffs from diffs.com-compatible components.

+
+
+ +
+ +
+

Expected Impact for End-Users

+

There should be no visible behavior change for ordinary app use. For development builds, Safari should now show an inspectable Dreamio or web.stremio.com target while the app is foregrounded, making the playback failure much easier to diagnose.

+
+ +
+

Validation

+
    +
  • Ran DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -list -project Dreamio.xcodeproj to confirm the Dreamio scheme.
  • +
  • Ran DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -project Dreamio.xcodeproj -scheme Dreamio -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build.
  • +
  • The Debug simulator build succeeded.
  • +
+
Manual real-device validation is still needed on kellcd: launch Dreamio, open Safari inspection, reproduce the Debridio VOD failure, and collect Console, Network, and media logs.
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • This does not fix playback. It adds the evidence-gathering surface for the next diagnosis step.
  • +
  • Safari Web Inspector availability still depends on the device, iOS version, Safari settings, and the app being a debug/development build.
  • +
  • Redaction is intentionally conservative but cannot prove every possible secret shape is removed. It strips common URL and token forms before logging.
  • +
  • The first xcodebuild attempt failed because the active developer directory pointed at Command Line Tools. Re-running with DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer succeeded.
  • +
+
+ +
+

Follow-up Work

+
    +
  • Use Safari Web Inspector on kellcd to capture the failing stream request and media error details.
  • +
  • File a follow-up Beads issue if the evidence points to a native-player fallback, MIME/header adjustment, or hosted Stremio compatibility gap.
  • +
  • Consider adding a temporary debug menu to toggle diagnostics if the log volume gets noisy during broader testing.
  • +
+
+
+ +