Repository navigation
feat(cloud): the iPhone app reads its home from FTW Cloud first (C3) #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0e84efa
feat(protocol): copy the FTW Cloud names from the registry
frahlg f69ffab
feat(cloud): read and age a box-signed home from FTW Cloud
frahlg 2f66504
feat(cloud): the app reads its home from FTW Cloud first
frahlg fca88b0
feat(cloud): ask again after a refusal, log each layer, and a live test
frahlg 6e97d19
fix(cloud): FTW Cloud turned off stays off after a restart
frahlg f3c2ff6
fix(app): honest band at start, prompt command failure, a calmer clock
frahlg 329dfce
fix(app): the band and the cards read one snapshot; time to live per …
frahlg 1d77dde
Merge main (#16, #17, #22, #23) into the app's FTW Cloud reading (C3)
frahlg 77dba30
fix(app): Swedish help when a connect fails; prices keep the home's zone
frahlg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import FTWKit | ||
| import SwiftUI | ||
|
|
||
| /// The one screen, at first start: the app uses FTW Cloud, and statistics | ||
| /// are a separate choice, off until ticked. An owner's phone turns FTW Cloud | ||
| /// on at the box when the person goes on; a viewer is told to ask the owner. | ||
| struct CloudConsentView: View { | ||
| let app: AppModel | ||
| @Bindable var consent: CloudConsent | ||
|
|
||
| private var audience: CloudConsent.Audience { | ||
| app.site.map(CloudConsent.audience) ?? .unknown | ||
| } | ||
|
|
||
| var body: some View { | ||
| List { | ||
| Section { | ||
| VStack(alignment: .leading, spacing: 12) { | ||
| Image(systemName: "cloud.fill") | ||
| .font(.largeTitle) | ||
| .foregroundStyle(Theme.accent) | ||
| .symbolEffect(.bounce, value: consent.answered) | ||
| .accessibilityHidden(true) | ||
| Text(CloudWords.text("The FTW app uses FTW Cloud")) | ||
| .font(.title2.weight(.bold)) | ||
| Text(CloudWords.text("FTW Cloud is part of the service. It keeps a copy of your home, signed by your box, so the app opens at once and stays up to date.")) | ||
| Text(CloudWords.text("Your box still decides. Commands go straight to it, never through FTW Cloud.")) | ||
| .foregroundStyle(Theme.fgDim) | ||
| } | ||
| .padding(.vertical, 4) | ||
| } | ||
|
|
||
| switch audience { | ||
| case .viewer: | ||
| Section { | ||
| Label(CloudWords.text("Ask the owner of this home to turn on FTW Cloud."), systemImage: "person.crop.circle.badge.questionmark") | ||
| } | ||
| case .owner, .ownerLater, .unknown: | ||
| Section { | ||
| Toggle(CloudWords.text("Contribute to statistics"), isOn: $consent.contributeStats) | ||
| } footer: { | ||
| Text(CloudWords.text("Hourly energy totals, without your name or address, help make FTW better. Off unless you turn it on.")) | ||
| } | ||
| if audience == .ownerLater { | ||
| Section { | ||
| Label(CloudWords.text("Your box turns on FTW Cloud after its next update."), systemImage: "arrow.down.circle") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Section { | ||
| Link(destination: Origin.privacyURL()) { | ||
| Label(CloudWords.text("Privacy policy"), systemImage: "hand.raised") | ||
| } | ||
| } footer: { | ||
| Text(CloudWords.text("You can turn FTW Cloud off in About.")) | ||
| } | ||
| } | ||
| .scrollContentBackground(.hidden) | ||
| .background(Theme.surface) | ||
| .safeAreaInset(edge: .bottom) { | ||
| Button(CloudWords.text("Continue")) { | ||
| Task { await consent.accept(app.site) } | ||
| } | ||
| .buttonStyle(FTWButtonStyle(kind: .primary)) | ||
| .padding(16) | ||
| .background(Theme.surface) | ||
| } | ||
| .animation(Theme.spring, value: audience) | ||
| .sensoryFeedback(.success, trigger: consent.answered) | ||
| .interactiveDismissDisabled() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import FTWKit | ||
| import SwiftUI | ||
|
|
||
| extension View { | ||
| /// The FTW Cloud consent, once, over a real home, and the owner's grant | ||
| /// at the box once the box can take it. RootView's one line. | ||
| func cloudConsent(_ app: AppModel) -> some View { | ||
| modifier(CloudGate(app: app)) | ||
| } | ||
| } | ||
|
|
||
| private struct CloudGate: ViewModifier { | ||
| let app: AppModel | ||
| @State private var consent = CloudConsent() | ||
|
|
||
| func body(content: Content) -> some View { | ||
| content | ||
| .sheet(isPresented: Binding(get: { !app.needsPairing && consent.shouldAsk(app.site) }, set: { _ in })) { | ||
| CloudConsentView(app: app, consent: consent) | ||
| .presentationDragIndicator(.hidden) | ||
| .tint(Theme.accent) | ||
| } | ||
| // When the box answers, an owner who agreed turns FTW Cloud on | ||
| // there, once per home. | ||
| .task(id: ensureKey) { | ||
| guard let site = app.site else { return } | ||
| await consent.ensure(site) | ||
| } | ||
| } | ||
|
|
||
| private var ensureKey: String { | ||
| guard let site = app.site else { return "" } | ||
| return "\(site.siteId ?? "")-\(site.heardFromBox)-\(site.cloudOn)-\(consent.answered)" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import FTWKit | ||
| import SwiftUI | ||
|
|
||
| /// FTW Cloud in About: on or off, and the receipt once the copy is erased. | ||
| /// An owner changes it at the box over Noise; a viewer sees it. | ||
| struct CloudSettingsView: View { | ||
| let app: AppModel | ||
| @State private var consent = CloudConsent() | ||
|
|
||
| var body: some View { | ||
| List { | ||
| if let site = app.site { | ||
| switch CloudConsent.audience(site) { | ||
| case .owner: | ||
| owner(site) | ||
| case .ownerLater: | ||
| Section { Text(CloudWords.text("Your box can't use FTW Cloud yet.")) } | ||
| case .viewer, .unknown: | ||
| Section { | ||
| Text(CloudWords.text(site.cloudOn ? "FTW Cloud is on for this home." : "FTW Cloud is off for this home.")) | ||
| } footer: { | ||
| Text(CloudWords.text("Only the owner of this home can change this.")) | ||
| } | ||
| } | ||
| } | ||
| Section { | ||
| Link(destination: Origin.privacyURL()) { | ||
| Label(CloudWords.text("Privacy policy"), systemImage: "hand.raised") | ||
| } | ||
| } | ||
| } | ||
| .scrollContentBackground(.hidden) | ||
| .background(Theme.surface) | ||
| .navigationTitle(CloudWords.text("FTW Cloud")) | ||
| .navigationBarTitleDisplayMode(.inline) | ||
| .task { | ||
| guard let site = app.site else { return } | ||
| await consent.refresh(site) | ||
| } | ||
| } | ||
|
|
||
| @ViewBuilder | ||
| private func owner(_ site: SiteModel) -> some View { | ||
| let box = consent.box | ||
| let on = box?.on ?? site.cloudOn | ||
| Section { | ||
| Toggle(CloudWords.text("FTW Cloud"), isOn: Binding(get: { on }, set: { want in | ||
| Task { | ||
| if want { await consent.turnOn(site) } else { await followErasure(site) } | ||
| } | ||
| })) | ||
| .disabled(consent.working || box?.erasing == true) | ||
| if on { | ||
| Label(CloudWords.text(site.cloudStatus?.isOpen == true || box?.connected == true ? "Connected" : "Not connected"), systemImage: "dot.radiowaves.left.and.right") | ||
| .foregroundStyle(Theme.fgDim) | ||
| } | ||
| if box?.erasing == true { | ||
| Label(CloudWords.text("Erasing the copy"), systemImage: "hourglass") | ||
| } else if !on, let at = box?.erasedAtMs { | ||
| Label(CloudWords.format("Copy erased %@", AppWords.day(at, nowMs: Date().timeIntervalSince1970 * 1000, zone: .current)), systemImage: "checkmark.seal") | ||
| .foregroundStyle(Theme.fgDim) | ||
| } | ||
| } footer: { | ||
| VStack(alignment: .leading, spacing: 4) { | ||
| Text(CloudWords.text("Turning it off erases the copy in FTW Cloud. Your box shows when it is gone.")) | ||
| if let help = consent.help { Text(help).foregroundStyle(Theme.importing) } | ||
| } | ||
| } | ||
| if on { | ||
| Section { | ||
| Toggle(CloudWords.text("Contribute to statistics"), isOn: Binding(get: { box?.stats ?? false }, set: { want in | ||
| Task { await consent.setStats(want, site) } | ||
| })) | ||
| .disabled(consent.working) | ||
| } footer: { | ||
| Text(CloudWords.text("Hourly energy totals, without your name or address, help make FTW better. Off unless you turn it on.")) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Off, then the receipt: the box has it once the service confirms the | ||
| /// copy is gone, usually within seconds. Asked a few times after the | ||
| /// tap, never on a timer of its own. | ||
| private func followErasure(_ site: SiteModel) async { | ||
| guard await consent.turnOff(site) else { return } | ||
| for _ in 0..<5 where consent.box?.erasing == true { | ||
| try? await Task.sleep(for: .seconds(2)) | ||
| await consent.refresh(site) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,36 +1,15 @@ | ||
| import Darwin | ||
| import os | ||
| import FTWKit | ||
|
|
||
| /// How long from the process starting to each first thing on screen, | ||
| /// logged once each and marked for Instruments. Read them with: | ||
| /// logged once each and marked for Instruments: the first frame, the first | ||
| /// content, and (from SiteModel) the first live reading. Read them with: | ||
| /// | ||
| /// xcrun simctl spawn <udid> log show --last 2m \ | ||
| /// --predicate 'subsystem == "energy.ftw.app" AND category == "launch"' | ||
| @MainActor | ||
| enum LaunchClock { | ||
| static let log = Logger(subsystem: "energy.ftw.app", category: "launch") | ||
| static let signposter = OSSignposter(logger: log) | ||
| private static var seen: Set<String> = [] | ||
|
|
||
| /// Once per event: the first frame, the first content, the stream. | ||
| /// Once per event: the first frame, the first content. | ||
| static func mark(_ event: StaticString) { | ||
| let name = "\(event)" | ||
| guard seen.insert(name).inserted else { return } | ||
| signposter.emitEvent(event) | ||
| if let ms = sinceProcessStartMs() { | ||
| log.notice("\(name, privacy: .public) \(Int(ms.rounded()), privacy: .public) ms after process start") | ||
| } | ||
| } | ||
|
|
||
| /// The kernel's record of when this process started. | ||
| static func sinceProcessStartMs() -> Double? { | ||
| var info = kinfo_proc() | ||
| var size = MemoryLayout<kinfo_proc>.stride | ||
| var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] | ||
| guard sysctl(&mib, u_int(mib.count), &info, &size, nil, 0) == 0 else { return nil } | ||
| let start = info.kp_proc.p_starttime | ||
| var now = timeval() | ||
| gettimeofday(&now, nil) | ||
| return Double(now.tv_sec - start.tv_sec) * 1000 + Double(now.tv_usec - start.tv_usec) / 1000 | ||
| ProcessClock.markOnce(event) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If consent is accepted while the box is
.ownerLater, a later hello after the box updates can addcapCloudLinkwithout changing any component of this task ID:siteId,heardFromBox(already true),cloudOn(still false), andanswered(already true). SwiftUI therefore does not rerunconsent.ensure, and the promised automatic grant is never sent until the view or app is recreated; include the relevant capability or derived audience in the task identity.Useful? React with 👍 / 👎.