From b9e1fa8755b1ab4ef1f58ecc12bbda29fb0c3690 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 16 Oct 2026 11:03:43 +0200 Subject: [PATCH] push: copy the agents' kinds from srcfl/ftw's catalogue srcfl/ftw#1642 added agent.added, agent.request, agent.expiring, approval.key.set and owner.added to the push catalogue; the app's copy missed them, so srcfl/ftw's 'the registry has not drifted from the app' check is red on master. The catalogue is copied byte for byte, with the English it holds and the Swedish beside it. These kinds are owners' news and come as the box's notice: as an event they reach viewers too, so an event that names one reads as generic. Co-Authored-By: Claude Opus 5.5 --- .../FTWKit/Sources/FTWShared/PushAlert.swift | 17 ++++++++++-- .../FTWKit/Sources/FTWShared/PushText.swift | 10 +++++++ .../Tests/FTWSharedTests/PushAlertTests.swift | 8 ++++++ protocol/push-catalogue.yaml | 26 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift b/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift index 2a2d4c1..72e681a 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift @@ -26,6 +26,13 @@ public enum PushKind { public static let boxUnreachable = "box.unreachable" public static let cloudGranted = "cloud.granted" public static let cloudRevoked = "cloud.revoked" + /// Access from outside the house, for every owner (docs/agents.md in + /// srcfl/ftw). They come as the box's `notice`, never as an event. + public static let agentAdded = "agent.added" + public static let agentRequest = "agent.request" + public static let agentExpiring = "agent.expiring" + public static let approvalKeySet = "approval.key.set" + public static let ownerAdded = "owner.added" /// The box asks which car is on a charger. The buttons, one per car, /// come with the car app (docs/cars.md in srcfl/ftw). public static let carWhich = "car.which" @@ -33,8 +40,13 @@ public enum PushKind { /// In the catalogue's order. public static let all = [ chargingConnected, chargingSessionComplete, chargingGoalAtRisk, chargingInterrupted, updateInstalled, - driverOffline, fuseOverLimit, boxUnreachable, cloudGranted, cloudRevoked, carWhich, + driverOffline, fuseOverLimit, boxUnreachable, cloudGranted, cloudRevoked, + agentAdded, agentRequest, agentExpiring, approvalKeySet, ownerAdded, carWhich, ] + + /// The kinds only owners may read. A box sends them as a `notice`; an + /// `event` that names one reaches viewers too, so it reads as generic. + public static let notices: Set = [agentAdded, agentRequest, agentExpiring, approvalKeySet, ownerAdded] } /// What a push says once checked. Only `read` makes one, so a kind that has @@ -134,7 +146,8 @@ public struct PushAlert: Sendable, Equatable { } if let ts = ev.ts { at = min(at, ts) } let age = nowMs.subtractingReportingOverflow(at) - guard PushKind.all.contains(ev.kind), ev.kind != PushKind.boxUnreachable, !age.overflow, age.partialValue <= maxAgeMs else { + guard PushKind.all.contains(ev.kind), ev.kind != PushKind.boxUnreachable, !PushKind.notices.contains(ev.kind), + !age.overflow, age.partialValue <= maxAgeMs else { return generic } return PushAlert(.event, home: home, kind: ev.kind, args: ev.args, src: ev.src, lp: ev.lp, boot: ev.boot.map { Data($0) }, seq: ev.seq, atMs: at) diff --git a/appleApp/FTWKit/Sources/FTWShared/PushText.swift b/appleApp/FTWKit/Sources/FTWShared/PushText.swift index 20415fa..e5beca7 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushText.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushText.swift @@ -76,6 +76,11 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Your box is out of reach", body: "It stopped answering. Power cut, or the internet at home is down."), PushKind.cloudGranted: Sentence(title: "FTW Cloud is on", body: "Your box now shares a copy of its readings with FTW Cloud. You can turn it off in the app or on the box."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud is off", body: "Your box stopped sharing with FTW Cloud and asked it to delete its copy."), + PushKind.agentAdded: Sentence(title: "New access to your box", body: "You gave an assistant or Sourceful access. See who has access in the app."), + PushKind.agentRequest: Sentence(title: "Waiting for your answer", body: "An assistant asks to change a setting. Open FTW to confirm or decline."), + PushKind.agentExpiring: Sentence(title: "Access ends tomorrow", body: "An assistant's or Sourceful's access to your box runs out in a day. Renew it in the app if it should go on."), + PushKind.approvalKeySet: Sentence(title: "A phone can now approve access", body: "A phone set up the key it uses to approve access to your box. If that was not you, remove the phone in the app."), + PushKind.ownerAdded: Sentence(title: "A new owner", body: "A phone was paired as an owner of your box. If that was not you, remove it in the app."), PushKind.carWhich: Sentence(title: "Which car is plugged in?", body: "Tap the car so the box plans for its level and goal."), ] @@ -90,6 +95,11 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Din box går inte att nå", body: "Den har slutat svara. Strömavbrott, eller så är internet hemma nere."), PushKind.cloudGranted: Sentence(title: "FTW Cloud är på", body: "Din box delar nu en kopia av sina mätvärden med FTW Cloud. Du kan stänga av det i appen eller på boxen."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud är av", body: "Din box har slutat dela med FTW Cloud och bett den radera sin kopia."), + PushKind.agentAdded: Sentence(title: "Ny åtkomst till din box", body: "En assistent eller Sourceful har fått åtkomst. Se vem som har åtkomst i appen."), + PushKind.agentRequest: Sentence(title: "Väntar på ditt svar", body: "En assistent vill ändra en inställning. Öppna FTW för att godkänna eller avböja."), + PushKind.agentExpiring: Sentence(title: "Åtkomsten upphör i morgon", body: "En assistents eller Sourcefuls åtkomst till din box går ut om ett dygn. Förnya den i appen om den ska fortsätta."), + PushKind.approvalKeySet: Sentence(title: "En telefon kan nu godkänna åtkomst", body: "En telefon har satt upp nyckeln den använder för att godkänna åtkomst till din box. Var det inte du, ta bort telefonen i appen."), + PushKind.ownerAdded: Sentence(title: "En ny ägare", body: "En telefon har parats som ägare av din box. Var det inte du, ta bort den i appen."), PushKind.carWhich: Sentence(title: "Vilken bil är inkopplad?", body: "Tryck på bilen så planerar boxen efter dess nivå och mål."), ] diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift b/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift index 4787756..5c5568a 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift +++ b/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift @@ -189,6 +189,14 @@ import Testing #expect(a.src == nil) } + /// Access news is for owners only. A box sends it as a `notice`; as an + /// `event` it also reaches viewers, so it reads as generic. + @Test func accessNewsAsAnEventIsGeneric() { + for kind in PushKind.notices { + #expect(Self.read(Self.envelope(Self.body(kind: kind))).verdict == .generic, "\(kind)") + } + } + /// Unknown keys are a newer box, not a fault. @Test func unknownKeysAreIgnored() { #expect(Self.read(Self.envelope(Self.body([("later", .m([("x", .u(1))]))]))).verdict == .event) diff --git a/protocol/push-catalogue.yaml b/protocol/push-catalogue.yaml index 2693e2f..0c9e8c5 100644 --- a/protocol/push-catalogue.yaml +++ b/protocol/push-catalogue.yaml @@ -60,6 +60,32 @@ events: # To every owner, when FTW Cloud is turned off at either door. title: FTW Cloud is off body: "Your box stopped sharing with FTW Cloud and asked it to delete its copy." + - kind: agent.added + # To every owner, when an owner gives an assistant, Sourceful support or + # the release check access to the box. Access from outside the house is + # news every owner should hear, as with cloud.granted. + title: New access to your box + body: "You gave an assistant or Sourceful access. See who has access in the app." + - kind: agent.request + # To every owner, when an agent asks for a durable change. It waits for + # one of them to confirm it, and runs out after twelve hours. + title: Waiting for your answer + body: "An assistant asks to change a setting. Open FTW to confirm or decline." + - kind: agent.expiring + # To every owner, a day before an agent's grant runs out, so access that + # should go on is renewed and access that should end can be left to end. + title: Access ends tomorrow + body: "An assistant's or Sourceful's access to your box runs out in a day. Renew it in the app if it should go on." + - kind: approval.key.set + # To every owner, when a phone registers or replaces the key that + # approves access. A new key is a new way to let others in. + title: A phone can now approve access + body: "A phone set up the key it uses to approve access to your box. If that was not you, remove the phone in the app." + - kind: owner.added + # To every owner, when a phone pairs as an owner. An owner can do + # anything, including give others access. + title: A new owner + body: "A phone was paired as an owner of your box. If that was not you, remove it in the app." - kind: car.which # When a car is plugged in and the box cannot tell which of its owners' # cars it is: no charger identity, no word from the car within a minute,